diff --git a/.gitignore b/.gitignore index cac762f1..55d3e1d3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ /vendor/ /.idea/ +/.phpunit.result.cache +/tests/.phpunit.result.cache +/code/deno \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index dad63571..de3a98b8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,5 +23,8 @@ RUN install-php-extensions \ WORKDIR /opt/project +# Change ownership to the local user +RUN chown -R ${user}:${group} /opt/project + # Switch to user USER ${uid} diff --git a/README.md b/README.md index 70b3b880..136eb0cb 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,22 @@ docker compose run --rm app ./bin/generate bmm:split openehr_base_1.3.0 docker compose run --rm app ./bin/generate bmm:split all ``` +Generate Deno/TypeScript/JavaScript library from BMM JSON files: +```bash +docker compose run --rm app ./bin/generate bmm:deno +``` +Examples +```bash +# Specific files +docker compose run --rm app ./bin/generate bmm:deno openehr_base_1.3.0 openehr_rm_1.2.0 +# Or generate for all schemas +docker compose run --rm app ./bin/generate bmm:deno all +``` +TODO, things not yet fixed in bmm:deno +- Split JS and TS to separate subtrees under code/deno do that we get JS in code/deno/js/ and TS in code/deno/ts instead of having them mixed +- if the "all" parameter is used, tehen versions risk overwriting each other. add another level per version so it follows the patterns code/deno/{{version}}/js/ and code/deno/{{version}}/ts/ +- Rename "Deno" to js-ts + ## Testing diff --git a/bin/Command/BmmDenoCommand.php b/bin/Command/BmmDenoCommand.php new file mode 100644 index 00000000..d662125e --- /dev/null +++ b/bin/Command/BmmDenoCommand.php @@ -0,0 +1,59 @@ +setName('bmm:deno'); + $this->setDescription('Generate Deno library based on indicated BMM schema(s).'); + $this->addArgument( + 'read', + InputArgument::IS_ARRAY, + 'BMM schema(s) to read; multiple schemas are supported when given as multiple arguments. ' + . 'Dependencies should be read first (i.e. first BASE then RM). ' + . 'Example: generate bmm:deno openehr_base_1.2.0 openehr_rm_1.1.0.', + ); + } + + +protected function execute(InputInterface $input, OutputInterface $output): int +{ + $toRead = $input->getArgument('read'); + if (empty($toRead)) { + $output->writeln('Please specify which BMM schema should be read. See usage with --help.'); + return Command::INVALID; + } + if ($toRead[0] === 'all') { + $toRead = array_map(fn($filename) => basename($filename, '.bmm.json'), glob(BmmJsonReader::DIR . '*.bmm.json')); + } + try { + $reader = new BmmJsonReader(); + foreach ($toRead as $schema) { + $reader->read($schema); + } + $writer = new CodeGenerator($reader); + $writer->addWriter(new BmmDenoWriter()); + $writer->generate(); + } catch (\Throwable $e) { + $output->writeln((string)$e); + return Command::FAILURE; + } + + return Command::SUCCESS; + } + +} diff --git a/bin/generate b/bin/generate index 8b5737ed..0fbb5cbe 100755 --- a/bin/generate +++ b/bin/generate @@ -16,6 +16,7 @@ use Console\Command\BmmJsonToAsciiDoc; use Console\Command\BmmJsonToClassJson; use Console\Command\XmiToBmm; use Console\Command\XmiToInternalModel; +use Console\Command\BmmDenoCommand; use Symfony\Component\Console\Application; try { @@ -27,6 +28,7 @@ try { $console->add(new BmmJsonToPlantUml()); $console->add(new BmmJsonToAsciiDoc()); $console->add(new BmmJsonToClassJson()); + $console->add(new BmmDenoCommand()); exit($console->run()); } catch (\Throwable $exception) { error_log((string)$exception); diff --git a/code/BMM-JSON/test_schema.bmm.json b/code/BMM-JSON/test_schema.bmm.json new file mode 100755 index 00000000..83947708 --- /dev/null +++ b/code/BMM-JSON/test_schema.bmm.json @@ -0,0 +1,44 @@ +{ + "bmm_version": "2.4", + "rm_publisher": "test", + "schema_name": "test_schema", + "rm_release": "1.0.0", + "schema_revision": "1.0.0", + "schema_lifecycle_state": "test", + "schema_description": "test", + "schema_author": "test", + "packages": [ + { + "name": "TestPackage", + "classes": [ + "TestClass" + ] + } + ], + "class_definitions": { + "TestClass": { + "name": "TestClass", + "ancestors": [], + "properties": [ + { + "_type": "P_BMM_SINGLE_PROPERTY", + "name": "test_property", + "type_def": { + "_type": "BMM_SIMPLE_TYPE", + "type": "STRING" + } + } + ], + "functions": [ + { + "name": "test_function", + "parameters": [], + "result": { + "_type": "BMM_SIMPLE_TYPE", + "type": "STRING" + } + } + ] + } + } +} diff --git a/docs/prd.md b/docs/prd.md new file mode 100644 index 00000000..2c07cd41 --- /dev/null +++ b/docs/prd.md @@ -0,0 +1,54 @@ +# Product Requirements Document: TypeScript/JavaScript openEHR Library Generator + +## 1. Introduction + +This document outlines the requirements for a new feature to be added to the openEHR code generator. The feature will enable the simultaneous generation of two versions of openEHR libraries: one in TypeScript and another in JavaScript with extensive JSDoc annotations. The generated libraries will be designed for use with the Deno runtime. + +## 2. Problem Statement + +Currently, the code generator can produce BMM JSON, BMM YAML, XMI Internal Model, and PlantUML files. There is a need to extend its capabilities to generate code for TypeScript and JavaScript developers who are working with openEHR data. This will enable them to work more efficiently and with greater type safety. + +## 3. Goals + +* To create a new generator that can produce openEHR libraries in both TypeScript and JavaScript. +* The generated JavaScript should be accompanied by extensive JSDoc annotations to provide a good developer experience for JavaScript users. +* The generated code should be compatible with the Deno runtime. +* The generated code should follow the specified coding conventions. +* The generator should be easy to use and well-documented. + +## 4. User Stories + +* As a TypeScript developer, I want to be able to generate openEHR libraries from BMM files so that I can work with openEHR data in a type-safe way. +* As a JavaScript developer, I want to be able to generate openEHR libraries from BMM files with extensive JSDoc annotations so that I can get good autocompletion and type checking in my editor. +* As a developer using Deno, I want to be able to use the generated openEHR libraries in my projects without having to transpile them or use a compatibility layer. + +## 5. Requirements + +### 5.1. Functional Requirements + +* The generator shall be implemented as a single writer that can produce both TypeScript and JavaScript output. +* The generator shall be triggered by a new command in the `bin/generate` script. +* The generated code shall be compatible with the Deno runtime and its module system. +* The generator shall produce JSDoc 3 annotations for the JavaScript output, including namepaths and tags for assertions, invariants, preconditions, and postconditions when available from the BMM source files. +* The generated code shall follow the specified coding conventions: + * `snake_case` for methods. + * `CAPITALIZED` class names for openEHR types. + * `CamelCase` for utility/helper classes and methods that are not part of the openEHR specifications. +* The generator shall not introduce any external dependencies other than those provided by the Deno runtime or modern browsers. + +### 5.2. Non-Functional Requirements + +* The generated code should be well-formatted and easy to read. +* The generator should be performant and be able to handle large BMM files. +* The generator should be well-tested to ensure the correctness of the generated code. + +## 6. Future Enhancements + +* Support for publishing the generated libraries to a Deno-friendly package registry. +* Support for generating code for other languages, such as Python or C#. +* Support for generating code from other input formats, such as Archetype Definition Language (ADL). + +## 7. Out of Scope + +* The implementation of a full openEHR validation library. The generated code will only represent the data structures and will not include any validation logic. +* The implementation of a user interface for the generator. The generator will only be available as a command-line tool. diff --git a/src/Model/Bmm/BmmSingleFunctionParameter.php b/src/Model/Bmm/BmmSingleFunctionParameter.php index 166dc904..3d4d42bc 100644 --- a/src/Model/Bmm/BmmSingleFunctionParameter.php +++ b/src/Model/Bmm/BmmSingleFunctionParameter.php @@ -1,73 +1,63 @@ - - */ public function jsonSerialize(): array { - return array_filter([ - '_type' => 'P_BMM_SINGLE_FUNCTION_PARAMETER', 'name' => $this->name, - 'documentation' => $this->documentation, - 'is_nullable' => $this->isNullable, - 'type' => $this->type, + 'type_def' => $this->typeDef, ]); } - /** - * @return TaggedValue - */ - public function yamlSerialize(): TaggedValue + public function yamlSerialize(): array { - return new TaggedValue('P_BMM_SINGLE_FUNCTION_PARAMETER', array_filter([ + return array_filter([ 'name' => $this->name, - 'documentation' => $this->documentation, - 'is_nullable' => $this->isNullable, - 'type' => $this->type, - ])); + 'type_def' => $this->typeDef->yamlSerialize(), + ]); } - /** - * Create a BMMSingleFunctionParameter from a JSON array - * - * @param array $data - * @return self - */ public static function fromArray(array $data): self { + $typeDefData = null; + + // Case 1: The modern 'type_def' object exists. + if (isset($data['type_def']) && is_array($data['type_def'])) { + $typeDefData = $data['type_def']; + } + // Case 2: The legacy 'type' string exists. + elseif (isset($data['type'])) { + $typeDefData = [ + '_type' => 'BMM_SIMPLE_TYPE', + 'type' => $data['type'], + ]; + } + // Case 3: Neither exists, so we must default to 'ANY' to prevent a crash. + else { + $typeDefData = [ + '_type' => 'BMM_SIMPLE_TYPE', + 'type' => 'ANY', + ]; + } + return new self( name: $data['name'], - type: $data['type'] ?? 'Any', - documentation: $data['documentation'] ?? null, - isNullable: $data['is_nullable'] ?? false, + typeDef: AbstractBmmType::fromArray($typeDefData) ); } } diff --git a/src/Model/Bmm/BmmSingleFunctionParameterOpen.php b/src/Model/Bmm/BmmSingleFunctionParameterOpen.php index a06ea159..671be3f8 100644 --- a/src/Model/Bmm/BmmSingleFunctionParameterOpen.php +++ b/src/Model/Bmm/BmmSingleFunctionParameterOpen.php @@ -1,30 +1,22 @@ - 'P_BMM_SINGLE_FUNCTION_PARAMETER_OPEN', 'name' => $this->name, - 'documentation' => $this->documentation, - 'is_nullable' => $this->isNullable, 'type' => $this->type, ]); } /** - * @return TaggedValue + * @return array */ - public function yamlSerialize(): TaggedValue + public function yamlSerialize(): array { - return new TaggedValue('P_BMM_SINGLE_FUNCTION_PARAMETER_OPEN', array_filter([ - 'name' => $this->name, - 'documentation' => $this->documentation, - 'is_nullable' => $this->isNullable, - 'type' => $this->type, - ])); + return $this->jsonSerialize(); } /** - * Create a BMMSingleFunctionParameter from a JSON array - * * @param array $data * @return self */ @@ -65,8 +47,6 @@ public static function fromArray(array $data): self return new self( name: $data['name'], type: $data['type'], - documentation: $data['documentation'] ?? null, - isNullable: $data['is_nullable'] ?? false, ); } -} +} \ No newline at end of file diff --git a/src/Model/Bmm/BmmSingleProperty.php b/src/Model/Bmm/BmmSingleProperty.php index f3bfa7f8..b9ad5caf 100644 --- a/src/Model/Bmm/BmmSingleProperty.php +++ b/src/Model/Bmm/BmmSingleProperty.php @@ -1,72 +1,65 @@ - - */ public function jsonSerialize(): array { return array_filter([ - '_type' => 'P_BMM_SINGLE_PROPERTY', 'name' => $this->name, - 'documentation' => $this->documentation, - 'is_mandatory' => $this->isMandatory, - 'type' => $this->type, + 'type_def' => $this->typeDef, ]); } - /** - * @return TaggedValue - */ - public function yamlSerialize(): TaggedValue + public function yamlSerialize(): array { - return new TaggedValue('P_BMM_SINGLE_PROPERTY', array_filter([ + return array_filter([ 'name' => $this->name, - 'documentation' => $this->documentation, - 'is_mandatory' => $this->isMandatory, - 'type' => $this->type, - ])); + 'type_def' => $this->typeDef->yamlSerialize(), + ]); } - /** - * Create a BMMSingleProperty from a JSON array - * - * @param array $data - * @return self - */ public static function fromArray(array $data): self { + $typeDefData = null; + + // Case 1: The modern 'type_def' object exists. + if (isset($data['type_def']) && is_array($data['type_def'])) { + $typeDefData = $data['type_def']; + } + // Case 2: The legacy 'type' string exists. + elseif (isset($data['type'])) { + $typeDefData = [ + '_type' => 'BMM_SIMPLE_TYPE', + 'type' => $data['type'], + ]; + } + // Case 3: Neither exists, so we must default to 'ANY' to prevent a crash. + else { + $typeDefData = [ + '_type' => 'BMM_SIMPLE_TYPE', + 'type' => 'ANY', + ]; + // also log a warning here + error_log("BMM schema '{$data['name']}' probably has an invalid type definition (neither type_def nor type found). Defaulting to 'ANY'."); + } + return new self( name: $data['name'], - type: $data['type'] ?? 'Any', - documentation: $data['documentation'] ?? null, - isMandatory: $data['is_mandatory'] ?? false, + typeDef: AbstractBmmType::fromArray($typeDefData) ); } } diff --git a/src/Writer/BmmDenoWriter.php b/src/Writer/BmmDenoWriter.php new file mode 100644 index 00000000..c590edfe --- /dev/null +++ b/src/Writer/BmmDenoWriter.php @@ -0,0 +1,378 @@ +assureOutputDir(); + /** @var BmmSchema $schema */ + foreach ($this->reader->files as $schema) { + /** @var BmmPackage $package */ + foreach ($schema->packages as $package) { + if (count($package->classes)) { + $this->createPackage( + $package, + $schema + ); + } + } + } + } + + private function createPackage( + BmmPackage $package, + BmmSchema $schema, + string $namePrefix = '' + ): void { + $name = $namePrefix . $package->name; + $packageDir = self::DIR . str_replace('.', DIRECTORY_SEPARATOR, $name); + $this->assureOutputDir($packageDir); + + foreach ($package->classes as $className) { + $class = $schema->classDefinitions->get($className); + if ($class instanceof BmmClass) { + $this->createClassFiles( + $class, + $packageDir + ); + } + } + + foreach ($package->packages as $subPackage) { + $this->createPackage( + $subPackage, + $schema, + $name . '.' + ); + } + } + + private function createClassFiles( + BmmClass $class, + string $packageDir + ): void { + $this->createTsFile( + $class, + $packageDir + ); + $this->createJsFile( + $class, + $packageDir + ); + } + + private function createTsFile( + BmmClass $class, + string $packageDir + ): void { + $tsContent = $this->generateTsContent($class); + $tsFilePath = $packageDir . DIRECTORY_SEPARATOR . $this->formatClassName($class->name, false) . '.ts'; + file_put_contents( + $tsFilePath, + $tsContent + ); + } + + private function createJsFile( + BmmClass $class, + string $packageDir + ): void { + $jsContent = $this->generateJsContent($class); + $jsFilePath = $packageDir . DIRECTORY_SEPARATOR . $this->formatClassName($class->name, false) . '.js'; + file_put_contents( + $jsFilePath, + $jsContent + ); + } + + private function generateTsContent(BmmClass $class): string + { + $imports = []; + $className = $this->formatClassName($class->name); + + $content = "/**\n * " . $class->documentation . "\n */\n"; + $content .= "class " . $className; + + if (!empty($class->ancestors)) { + $ancestor = $this->formatClassName($class->ancestors[0]); + $imports[] = $this->formatClassName($class->ancestors[0], false); + $content .= " extends " . $ancestor; + } + + $content .= " {\n"; + + foreach ($class->properties as $property) { + $type = $this->mapBmmPropertyToTsType($property, $imports); + $content .= " " . $this->formatPropertyName($property->name) . ": " . $type . ";\n"; + } + + $content .= "\n"; + $content .= " constructor() {\n"; + if (!empty($class->ancestors)) { + $content .= " super();\n"; + } + $content .= " }\n"; + $content .= "\n"; + + foreach ($class->functions as $function) { + $content .= $this->generateTsMethod($function, $imports); + } + + $content .= "}\n"; + + $importContent = ""; + foreach (array_unique($imports) as $import) { + $importContent .= "import { " . $this->formatClassName($import) . " } from './" . $import . ".ts';\n"; + } + + return $importContent . "\n" . $content; + } + + private function generateJsContent(BmmClass $class): string + { + $imports = []; + $className = $this->formatClassName($class->name); + $content = "/**\n * @class " . $className . "\n * @description " . $class->documentation . "\n */\n"; + $content .= "class " . $className; + + if (!empty($class->ancestors)) { + $ancestor = $this->formatClassName($class->ancestors[0]); + $imports[] = $this->formatClassName($class->ancestors[0], false); + $content .= " extends " . $ancestor; + } + + $content .= " {\n"; + + foreach ($class->properties as $property) { + $type = $this->mapBmmPropertyToJsType($property, $imports); + $content .= " /**\n"; + $content .= " * @type {" . $type . "}\n"; + $content .= " */\n"; + $content .= " " . $this->formatPropertyName($property->name) . ";\n"; + } + + $content .= "\n"; + $content .= " constructor() {\n"; + if (!empty($class->ancestors)) { + $content .= " super();\n"; + } + $content .= " }\n"; + $content .= "\n"; + + foreach ($class->functions as $function) { + $content .= $this->generateJsMethod($function, $imports); + } + + $content .= "}\n"; + + $importContent = ""; + foreach (array_unique($imports) as $import) { + $importContent .= "import { " . $this->formatClassName($import) . " } from './" . $import . ".js';\n"; + } + + return $importContent . "\n" . $content; + } + + private function generateTsMethod(BmmFunction $function, array &$imports): string + { + $methodName = $this->formatMethodName($function->name); + $parameters = []; + foreach ($function->parameters as $parameter) { + $parameters[] = $this->formatPropertyName($parameter->name) . ": " . $this->mapBmmTypeToTsType($parameter->typeDef, $imports); + } + $returnType = $this->mapBmmTypeToTsType($function->result, $imports); + + $content = " /**\n"; + $content .= " * " . $function->documentation . "\n"; + $content .= " */\n"; + $content .= " " . $methodName . "(" . implode(", ", $parameters) . "): " . $returnType . " {\n"; + $content .= " // TODO: Implement method\n"; + $content .= " return null;\n"; + $content .= " }\n\n"; + + return $content; + } + + private function generateJsMethod(BmmFunction $function, array &$imports): string + { + $methodName = $this->formatMethodName($function->name); + $parameters = []; + $paramDocs = []; + foreach ($function->parameters as $parameter) { + $paramName = $this->formatPropertyName($parameter->name); + $paramType = $this->mapBmmTypeToJsType($parameter->typeDef, $imports); + $parameters[] = $paramName; + $paramDocs[] = " * @param {" . $paramType . "} " . $paramName; + } + $returnType = $this->mapBmmTypeToJsType($function->result, $imports); + + $content = " /**\n"; + $content .= " * " . $function->documentation . "\n"; + $content .= implode("\n", $paramDocs) . "\n"; + foreach ($function->preConditions as $preCondition) { + $content .= " * @pre " . $preCondition . "\n"; + } + foreach ($function->postConditions as $postCondition) { + $content .= " * @post " . $postCondition . "\n"; + } + $content .= " * @returns {" . $returnType . "}\n"; + $content .= " */\n"; + $content .= " " . $methodName . "(" . implode(", ", $parameters) . ") {\n"; + $content .= " // TODO: Implement method\n"; + $content .= " return null;\n"; + $content .= " }\n\n"; + + return $content; + } + + private function mapBmmPropertyToTsType(AbstractBmmProperty $property, array &$imports): string + { + if ($property instanceof BmmSingleProperty) { + return $this->mapBmmTypeToTsType($property->typeDef, $imports); + } elseif ($property instanceof BmmContainerProperty) { + return "Array<" . $this->mapBmmTypeToTsType($property->typeDef, $imports) . ">"; + } elseif ($property instanceof BmmGenericProperty) { + return $this->mapBmmTypeToTsType($property->typeDef, $imports); + } elseif ($property instanceof BmmSinglePropertyOpen) { + return "any"; + } + return "any"; + } + + private function mapBmmPropertyToJsType(AbstractBmmProperty $property, array &$imports): string + { + if ($property instanceof BmmSingleProperty) { + return $this->mapBmmTypeToJsType($property->typeDef, $imports); + } elseif ($property instanceof BmmContainerProperty) { + return "Array<" . $this->mapBmmTypeToJsType($property->typeDef, $imports) . ">"; + } elseif ($property instanceof BmmGenericProperty) { + return $this->mapBmmTypeToJsType($property->typeDef, $imports); + } elseif ($property instanceof BmmSinglePropertyOpen) { + return "any"; + } + return "any"; + } + + private function mapBmmTypeToTsType($type, array &$imports): string + { + if ($type === null) { + return "void"; + } + + if ($type instanceof BmmContainerType) { + return "Array<" . $this->mapBmmTypeToTsType($type->typeDef, $imports) . ">"; + } elseif ($type instanceof BmmGenericType) { + $typeParameters = []; + foreach ($type->genericParameterDefs as $param) { + $typeParameters[] = $this->mapBmmTypeToTsType($param, $imports); + } + $rootType = $this->formatClassName($type->rootType); + if (!in_array($this->formatClassName($type->rootType, false), $imports)) { + $imports[] = $this->formatClassName($type->rootType, false); + } + return $rootType . "<" . implode(", ", $typeParameters) . ">"; + } elseif ($type instanceof BmmSimpleType) { + $primitiveType = $this->mapBmmPrimitiveToTsType($type->type); + if ($primitiveType !== null) { + return $primitiveType; + } + $className = $this->formatClassName($type->type, false); + if (!in_array($className, $imports)) { + $imports[] = $className; + } + return $this->formatClassName($type->type); + } + + return "any"; + } + + private function mapBmmTypeToJsType($type, array &$imports): string + { + if ($type === null) { + return "void"; + } + + if ($type instanceof BmmContainerType) { + return "Array<" . $this->mapBmmTypeToJsType($type->typeDef, $imports) . ">"; + } elseif ($type instanceof BmmGenericType) { + $typeParameters = []; + foreach ($type->genericParameterDefs as $param) { + $typeParameters[] = $this->mapBmmTypeToJsType($param, $imports); + } + $rootType = $this->formatClassName($type->rootType); + if (!in_array($this->formatClassName($type->rootType, false), $imports)) { + $imports[] = $this->formatClassName($type->rootType, false); + } + return $rootType . "<" . implode(", ", $typeParameters) . ">"; + } elseif ($type instanceof BmmSimpleType) { + $primitiveType = $this->mapBmmPrimitiveToJsType($type->type); + if ($primitiveType !== null) { + return $primitiveType; + } + $className = $this->formatClassName($type->type, false); + if (!in_array($className, $imports)) { + $imports[] = $className; + } + return $this->formatClassName($type->type); + } + + return "any"; + } + + private function mapBmmPrimitiveToTsType(string $type): ?string + { + return match (strtolower($type)) { + 'string', 'iso8601_date', 'iso8601_date_time', 'iso8601_time', 'iso8601_duration', 'terminology_code' => 'string', + 'integer', 'integer64' => 'number', + 'real' => 'number', + 'boolean' => 'boolean', + default => null, + }; + } + + private function mapBmmPrimitiveToJsType(string $type): ?string + { + return match (strtolower($type)) { + 'string', 'iso8601_date', 'iso8601_date_time', 'iso8601_time', 'iso8601_duration', 'terminology_code' => 'string', + 'integer', 'integer64' => 'number', + 'real' => 'number', + 'boolean' => 'boolean', + default => null, + }; + } + + private function formatClassName(string $name, bool $capitalize = true): string + { + if ($capitalize) { + return strtoupper($name); + } + return $name; + } + + private function formatPropertyName(string $name): string + { + return lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', $name)))); + } + + private function formatMethodName(string $name): string + { + return strtolower(str_replace(' ', '_', str_replace('-', '_', $name))); + } +} diff --git a/tests/Writer/BmmDenoWriterTest.php b/tests/Writer/BmmDenoWriterTest.php new file mode 100755 index 00000000..88da572f --- /dev/null +++ b/tests/Writer/BmmDenoWriterTest.php @@ -0,0 +1,79 @@ +writer = new BmmDenoWriter(); + if (!is_dir(BmmDenoWriter::DIR)) { + mkdir(BmmDenoWriter::DIR, 0777, true); + } + } + + protected function tearDown(): void + { + if (is_dir(BmmDenoWriter::DIR)) { + $this->deleteDirectory(BmmDenoWriter::DIR); + } + } + + public function testCanBeInstantiated(): void + { + $this->assertInstanceOf(BmmDenoWriter::class, $this->writer); + } + + public function testWriteGeneratesCorrectFiles(): void + { + $reader = new BmmJsonReader(); + $reader->read('test_schema'); + + $this->writer->setReader($reader); + $this->writer->write(); + + $tsFile = BmmDenoWriter::DIR . 'TestPackage/TestClass.ts'; + $jsFile = BmmDenoWriter::DIR . 'TestPackage/TestClass.js'; + + $this->assertFileExists($tsFile); + $this->assertFileExists($jsFile); + + $tsContent = file_get_contents($tsFile); + $jsContent = file_get_contents($jsFile); + + $this->assertStringContainsString('class TESTCLASS', $tsContent); + $this->assertStringContainsString('testProperty: string;', $tsContent); + $this->assertStringContainsString('test_function(): string', $tsContent); + + $this->assertStringContainsString('class TESTCLASS', $jsContent); + $this->assertStringContainsString('@type {string}', $jsContent); + $this->assertStringContainsString('testProperty;', $jsContent); + $this->assertStringContainsString('test_function()', $jsContent); + } + + private function deleteDirectory(string $dir): void + { + if (!is_dir($dir)) { + return; + } + + $items = array_diff(scandir($dir), ['.', '..']); + + foreach ($items as $item) { + $path = $dir . DIRECTORY_SEPARATOR . $item; + if (is_dir($path)) { + $this->deleteDirectory($path); + } else { + unlink($path); + } + } + + rmdir($dir); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100755 index 00000000..96b6c432 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,5 @@ +