From e36a30de4c91fc06a398867310b471cd6c1d045f Mon Sep 17 00:00:00 2001 From: David Lechner Date: Fri, 11 Mar 2022 13:14:19 -0600 Subject: [PATCH] pybricksMicropython/lib: add module This creates a new module for dealing with stuff specific to Pybricks MicroPython. --- src/editor/OpenButton.tsx | 3 +- src/editor/sagas.ts | 5 +- src/explorer/NewFileWizard.tsx | 71 ++++------------------------- src/explorer/actions.ts | 8 ++-- src/explorer/sagas.test.ts | 5 +- src/fileStorage/sagas.ts | 3 +- src/pybricksMicropython/lib.test.ts | 54 ++++++++++++++++++++++ src/pybricksMicropython/lib.ts | 66 +++++++++++++++++++++++++++ 8 files changed, 142 insertions(+), 73 deletions(-) create mode 100644 src/pybricksMicropython/lib.test.ts create mode 100644 src/pybricksMicropython/lib.ts diff --git a/src/editor/OpenButton.tsx b/src/editor/OpenButton.tsx index 4a745941..66be3c46 100644 --- a/src/editor/OpenButton.tsx +++ b/src/editor/OpenButton.tsx @@ -4,6 +4,7 @@ import React, { useContext } from 'react'; import { useDispatch } from 'react-redux'; import * as notificationActions from '../notifications/actions'; +import { pythonFileExtension } from '../pybricksMicropython/lib'; import OpenFileButton, { OpenFileButtonProps } from '../toolbar/OpenFileButton'; import { TooltipId } from '../toolbar/i18n'; import { EditorContext } from './Editor'; @@ -18,7 +19,7 @@ const OpenButton: React.FunctionComponent = (props) => { return ( , -): FileNameValidationResult { - if (existingFiles.includes(`${fileName}${extension}`)) { - return FileNameValidationResult.AlreadyExists; - } - - if (fileName.length === 0) { - return FileNameValidationResult.IsEmpty; - } - - if (fileName.match(/\s/)) { - return FileNameValidationResult.HasSpaces; - } - - if (fileName.endsWith(extension)) { - return FileNameValidationResult.HasFileExtension; - } - - if (!fileName.match(/^[a-zA-Z_]/)) { - return FileNameValidationResult.HasInvalidFirstCharacter; - } - - if (!fileName.match(/^[a-zA-Z0-9_-]+$/)) { - return FileNameValidationResult.HasInvalidCharacters; - } - - return FileNameValidationResult.IsOk; -} - type FileNameHelpTextProps = { validation: FileNameValidationResult; }; @@ -169,7 +116,7 @@ const NewFileWizard: React.VoidFunctionComponent = (props) = const handleFileNameChanged = (fileName: string) => { setFileNameValidation( - validateFileName(fileName, FileExtension.Python, fileNames), + validateFileName(fileName, pythonFileExtension, fileNames), ); setFileName(fileName); }; @@ -194,7 +141,7 @@ const NewFileWizard: React.VoidFunctionComponent = (props) = value={fileName} inputRef={fileNameInputRef} intent={fileNameIntent} - rightElement={{FileExtension.Python}} + rightElement={{pythonFileExtension}} onChange={(e) => handleFileNameChanged(e.target.value)} /> @@ -224,7 +171,7 @@ const NewFileWizard: React.VoidFunctionComponent = (props) = dispatch( explorerCreateNewFile( fileName, - FileExtension.Python, + pythonFileExtension, hubType, ), ); diff --git a/src/explorer/actions.ts b/src/explorer/actions.ts index 405b4ac9..7be2013f 100644 --- a/src/explorer/actions.ts +++ b/src/explorer/actions.ts @@ -2,12 +2,10 @@ // Copyright (c) 2022 The Pybricks Authors import { createAction } from '../actions'; +import { pythonFileExtension } from '../pybricksMicropython/lib'; /** Supported file extensions. */ -export enum FileExtension { - /** Python (.py) */ - Python = '.py', -} +type SupportedFileExtension = typeof pythonFileExtension; /** Supported hub types. */ export enum Hub { @@ -32,7 +30,7 @@ export enum Hub { * @param hub The type of hub this file is for. */ export const explorerCreateNewFile = createAction( - (fileName: string, fileExtension: FileExtension, hub: Hub) => ({ + (fileName: string, fileExtension: SupportedFileExtension, hub: Hub) => ({ type: 'explorer.action.createNewFile', fileName, fileExtension, diff --git a/src/explorer/sagas.test.ts b/src/explorer/sagas.test.ts index c5a0d101..a131fa61 100644 --- a/src/explorer/sagas.test.ts +++ b/src/explorer/sagas.test.ts @@ -2,14 +2,15 @@ // Copyright (c) 2022 The Pybricks Authors import { AsyncSaga } from '../../test'; -import { FileExtension, Hub, explorerCreateNewFile } from './actions'; +import { pythonFileExtension } from '../pybricksMicropython/lib'; +import { Hub, explorerCreateNewFile } from './actions'; import explorer from './sagas'; describe('handleExplorerCreateNewFile', () => { it('should dispatch fileStorage action', async () => { const saga = new AsyncSaga(explorer); - saga.put(explorerCreateNewFile('test', FileExtension.Python, Hub.Technic)); + saga.put(explorerCreateNewFile('test', pythonFileExtension, Hub.Technic)); const action = await saga.take(); expect(action).toMatchInlineSnapshot(` diff --git a/src/fileStorage/sagas.ts b/src/fileStorage/sagas.ts index a5ad4cb1..058c8548 100644 --- a/src/fileStorage/sagas.ts +++ b/src/fileStorage/sagas.ts @@ -9,6 +9,7 @@ import { eventChannel } from 'redux-saga'; import { call, fork, getContext, put, takeEvery } from 'typed-redux-saga/macro'; import Observable from 'zen-observable'; import { EditorType } from '../editor/Editor'; +import { pythonFileExtension, pythonFileMimeType } from '../pybricksMicropython/lib'; import { ensureError, timestamp } from '../utils'; import { fileStorageArchiveAllFiles, @@ -117,7 +118,7 @@ function* handleExportFile( suggestedName: action.fileName, types: [ { - accept: { 'text/x-python': '.py' }, + accept: { [pythonFileMimeType]: pythonFileExtension }, // TODO: translate description description: 'Python Files', }, diff --git a/src/pybricksMicropython/lib.test.ts b/src/pybricksMicropython/lib.test.ts new file mode 100644 index 00000000..6bbb0a54 --- /dev/null +++ b/src/pybricksMicropython/lib.test.ts @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2022 The Pybricks Authors + +import { FileNameValidationResult, pythonFileExtension, validateFileName } from './lib'; + +describe('validateFileName', () => { + it('should allow file names with underscores', () => { + expect(validateFileName('file_name', pythonFileExtension, [])).toBe( + FileNameValidationResult.IsOk, + ); + }); + + it('should allow file names with dashes', () => { + expect(validateFileName('file-name', pythonFileExtension, [])).toBe( + FileNameValidationResult.IsOk, + ); + }); + + it('should not allow empty strings', () => { + expect(validateFileName('', pythonFileExtension, [])).toBe( + FileNameValidationResult.IsEmpty, + ); + }); + + it('should not allow file names with spaces', () => { + expect(validateFileName('file name', pythonFileExtension, [])).toBe( + FileNameValidationResult.HasSpaces, + ); + }); + + it('should not allow file names that start with numbers', () => { + expect(validateFileName('1test', pythonFileExtension, [])).toBe( + FileNameValidationResult.HasInvalidFirstCharacter, + ); + }); + + it('should not allow file names with symbols', () => { + expect(validateFileName('test$', pythonFileExtension, [])).toBe( + FileNameValidationResult.HasInvalidCharacters, + ); + }); + + it('it should not allow file names that include the file extension', () => { + expect(validateFileName('test.py', pythonFileExtension, [])).toBe( + FileNameValidationResult.HasFileExtension, + ); + }); + + it('should not allow file names that match existing files', () => { + expect(validateFileName('test', pythonFileExtension, ['test.py'])).toBe( + FileNameValidationResult.AlreadyExists, + ); + }); +}); diff --git a/src/pybricksMicropython/lib.ts b/src/pybricksMicropython/lib.ts new file mode 100644 index 00000000..c093edf7 --- /dev/null +++ b/src/pybricksMicropython/lib.ts @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2022 The Pybricks Authors + +/** The Python file extension ('.py') */ +export const pythonFileExtension = '.py'; + +/** The Python file MIME type ('text/x-python') */ +export const pythonFileMimeType = 'text/x-python'; + +/** File name validation results. */ +export enum FileNameValidationResult { + /** The file name is acceptable. */ + IsOk, + /** The file name is an empty string. */ + IsEmpty, + /** The file name contains spaces. */ + HasSpaces, + /** The file name include the file file extension. */ + HasFileExtension, + /** The first character is not a letter or underscore. */ + HasInvalidFirstCharacter, + /** The file name has invalid characters. */ + HasInvalidCharacters, + /** A file with the same name already exists. */ + AlreadyExists, +} + +/** + * Validates the file name according to a number of criteria. + * + * @param fileName The file name (without extension). + * @param extension The file extension (including "."). + * @param existingFiles List of existing files. + * @returns The result of the validation. + */ +export function validateFileName( + fileName: string, + extension: string, + existingFiles: ReadonlyArray, +): FileNameValidationResult { + if (existingFiles.includes(`${fileName}${extension}`)) { + return FileNameValidationResult.AlreadyExists; + } + + if (fileName.length === 0) { + return FileNameValidationResult.IsEmpty; + } + + if (fileName.match(/\s/)) { + return FileNameValidationResult.HasSpaces; + } + + if (fileName.endsWith(extension)) { + return FileNameValidationResult.HasFileExtension; + } + + if (!fileName.match(/^[a-zA-Z_]/)) { + return FileNameValidationResult.HasInvalidFirstCharacter; + } + + if (!fileName.match(/^[a-zA-Z0-9_-]+$/)) { + return FileNameValidationResult.HasInvalidCharacters; + } + + return FileNameValidationResult.IsOk; +}