diff --git a/README.md b/README.md new file mode 100644 index 0000000..b0565d1 --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +# TS Generator + +This is a Drupal module, that generates Typescript type definitions for certain entities. It can optionally also generate cleaned up target type definitions and functions to convert objects from the initials types to the target types (parsers). + +## Installation + +```sh +composer require hoppinger/ts_generator +``` + +## Usage + +The generator runs as a Drush command that needs a configuration file. + +Create `.yml` file with the following contents: + +```yaml +target_directory: client/generated +entities: + input: + - node + - taxonomy_term +generate_parser: true +``` + +This file instructs the generator to generate the files in a directory `client/generated` (relative to the location of the configuration file), to generate types for the `node` and `taxonomy_term` entities and to generate target types and corresponding parsers. + +Trigger the generation using + +```sh +cd [PROJECT DIRECTORY]/web +../vendor/bin/drush ts_generator:generate [PATH TO CONFIGURATION FILE] +``` + +## Actual usage of the types + +This is an example of how you could use those types. You are not limited to this approach, of course. + +```ts +import { + InputEntity, + ParsedEntity, + ParsedInputEntity, +} from "./generated/types" + +import { + input_entity_parser, + input_entity_guard +} from "./generated/types" + +export type Result = T | "error" + +export async function drupalGetEntity(alias: string): Promise> { + const res = await fetch(`/${alias}?format=_json`), { method: "get", credentials: "include" }) + if (!res.ok) return "error" + + const json = await res.json() + if (!input_entity_guard(json)) return "error" + + const parsed = input_entity_parser(json as InputEntity) + return parsed +} +``` + +## Todo + +* Write more documentation on usage +* Write documentation on the internal workings and ways to extend the generator diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..053b156 --- /dev/null +++ b/composer.json @@ -0,0 +1,22 @@ +{ + "name": "hoppinger/ts_generator", + "description": "Drupal module to generate TypeScript code based on the REST Resources", + "type": "drupal-module", + "license": "GPL-2.0+", + "minimum-stability": "dev", + "homepage": "https://github.com/hoppinger/ts_generator", + "authors": [ + { + "name": "Rolf van de Krol", + "role": "Maintainer" + } + ], + "require": {}, + "extra": { + "drush": { + "services": { + "drush.services.yml": "^9" + } + } + } +} \ No newline at end of file diff --git a/drush.services.yml b/drush.services.yml new file mode 100644 index 0000000..ce68e6b --- /dev/null +++ b/drush.services.yml @@ -0,0 +1,5 @@ +services: + ts_generator.commands: + class: \Drupal\ts_generator\Commands\TsGeneratorCommands + tags: + - { name: drush.command } diff --git a/src/Commands/TsGeneratorCommands.php b/src/Commands/TsGeneratorCommands.php new file mode 100644 index 0000000..35c9cf1 --- /dev/null +++ b/src/Commands/TsGeneratorCommands.php @@ -0,0 +1,45 @@ +logger()->error(dt('The specified file does not exist.')); + } + + $working_directory = dirname($filename); + $settings = Settings::loadFile($filename); + + /** @var \Drupal\ts_generator\GeneratorInterface $generator */ + $generator = \Drupal::service('ts_generator.generator'); + + $entity_type_manager = \Drupal::entityTypeManager(); + + $result = new Result(); + $generator->generate($entity_type_manager, $settings, $result); + + $target_directory = $working_directory . '/' . $settings->get('target_directory'); + $result->write($target_directory); + } +} diff --git a/src/ComponentGenerator/Data/AnyGenerator.php b/src/ComponentGenerator/Data/AnyGenerator.php new file mode 100644 index 0000000..493f749 --- /dev/null +++ b/src/ComponentGenerator/Data/AnyGenerator.php @@ -0,0 +1,14 @@ +supportedDataType; + + return !isset($this->supportedDataType) || in_array($object->getDataType(), $supported); + } + + /** + * @inheritdoc + */ + public function generateType($object, Settings $settings, Result $result, ComponentResult $componentResult) { + /** @var \Drupal\Core\TypedData\DataDefinitionInterface $object */ + $componentResult->setComponent('base_type', $this->getDataType($object, $settings, $result, $componentResult)); + return $object->isRequired() ? $componentResult->getComponent('base_type') : $componentResult->getComponent('base_type') . ' | null'; + } +} \ No newline at end of file diff --git a/src/ComponentGenerator/Data/DateTimeGenerator.php b/src/ComponentGenerator/Data/DateTimeGenerator.php new file mode 100644 index 0000000..2f3c0e7 --- /dev/null +++ b/src/ComponentGenerator/Data/DateTimeGenerator.php @@ -0,0 +1,39 @@ +setComponent('base_target_type', ':/moment/Moment*:.Moment'); + return $object->isRequired() ? $componentResult->getComponent('base_target_type') : $componentResult->getComponent('base_target_type') . ' | null'; + } + + protected function generateParser($object, Settings $settings, Result $result, ComponentResult $componentResult) { + /** @var \Drupal\Core\TypedData\DataDefinitionInterface $object */ + $base_parser = $result->setComponent( + 'parser/moment_parser', + 'const moment_parser = (t: string): :/moment/Moment*:.Moment => :/moment/Moment*:(t)' + ); + $componentResult->setComponent('base_parser', $base_parser); + + if ($object->isRequired()) { + $name = 'required_date_time_parser'; + return $result->setComponent('parser/' . $name, 'const ' . $name . ' = (t: string): :/moment/Moment*:.Moment => ' . $base_parser . '(t)'); + } else { + $name = 'optional_date_time_parser'; + return $result->setComponent('parser/' . $name, 'const ' . $name . ' = (t: string | null): :/moment/Moment*:.Moment | null => t ? ' . $base_parser . '(t) : null'); + } + + } +} \ No newline at end of file diff --git a/src/ComponentGenerator/Data/FilterFormatGenerator.php b/src/ComponentGenerator/Data/FilterFormatGenerator.php new file mode 100644 index 0000000..4c4166d --- /dev/null +++ b/src/ComponentGenerator/Data/FilterFormatGenerator.php @@ -0,0 +1,38 @@ +entityTypeManager = $entityTypeManager; + } + + protected function generateFilterFormatObject($object, Settings $settings, Result $result, ComponentResult $componentResult) { + $filter_format_storage = $this->entityTypeManager->getStorage('filter_format'); + $filter_formats = $filter_format_storage->loadMultiple(); + + $filter_format_keys = []; + foreach ($filter_formats as $filter_format) { + $filter_format_keys[] = '\'' . $filter_format->id() . '\''; + } + + $filter_format_component = $result->setComponent('types/FilterFormat', "type FilterFormat = " . implode(' | ', $filter_format_keys)); + return $filter_format_component; + } + + protected function getDataType($object, Settings $settings, Result $result, ComponentResult $componentResult) { + return $this->generateFilterFormatObject($object, $settings, $result, $componentResult); + } +} \ No newline at end of file diff --git a/src/ComponentGenerator/Data/NumberGenerator.php b/src/ComponentGenerator/Data/NumberGenerator.php new file mode 100644 index 0000000..1f39bad --- /dev/null +++ b/src/ComponentGenerator/Data/NumberGenerator.php @@ -0,0 +1,15 @@ +entityTypeManager = $entityTypeManager; + $this->entityFieldManager = $entityFieldManager; + $this->entityTypeRepository = $entityTypeRepository; + } + + public function supportsGeneration($object) { + if (!($object instanceof ConfigEntityInterface)) { + return FALSE; + } + + try { + $entity_type_id = $this->entityTypeRepository->getEntityTypeFromClass(get_class($object)); + } catch (AmbiguousEntityClassException $e) { + return FALSE; + } catch (NoCorrespondingEntityClassException $e) { + return FALSE; + } + + $entity_type = $this->entityTypeManager->getDefinition($entity_type_id); + $bundle_of = $entity_type->getBundleOf(); + + if (empty($bundle_of)) { + return FALSE; + } else { + return TRUE; + } + } + + protected function getEntityId($object) { + $bundle_entity_type_id = $this->entityTypeRepository->getEntityTypeFromClass(get_class($object)); + $bundle_entity_type = $this->entityTypeManager->getDefinition($bundle_entity_type_id); + return $bundle_entity_type->getBundleOf(); + } + + protected function getName($object, Settings $settings, Result $result, ComponentResult $componentResult) { + /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $object */ + $entity_type_id = $this->getEntityId($object); + return Container::camelize($entity_type_id) . Container::camelize($object->id()); + } + + protected function getProperties($object, Settings $settings, Result $result, ComponentResult $component_result) { + /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $object */ + $entity_type_id = $this->getEntityId($object); + $entity_type = $this->entityTypeManager->getDefinition($entity_type_id); + + $bundle = $object->id(); + + $base_field_definitions = $this->entityFieldManager->getBaseFieldDefinitions($entity_type_id); + $field_definitions = $this->entityFieldManager->getFieldDefinitions($entity_type_id, $bundle); + + $_bundle_type = $this->generator->generate($base_field_definitions[$entity_type->getKey('bundle')], $settings, $result); + $bundle_type = $_bundle_type->getComponent('wrapper_type') . '<' . $_bundle_type->getComponent('specific_item_type') . "<" . json_encode($bundle) . ">>"; + + $properties = [ + $entity_type->getKey('bundle') => new ComponentResult([ + 'type' => $bundle_type, + 'target_type' => json_encode($bundle), + 'parser' => '((_: any): ' . json_encode($bundle) . ' => ' . json_encode($bundle) . ')', + ]) + ]; + + foreach ($field_definitions as $key => $field_definition) { + if ($field_definition->isInternal()) { + continue; + } + + $property_value = $this->generator->generate($field_definition, $settings, $result); + + if (!empty($base_field_definitions[$key])) { + $base_field_property_value = $this->generator->generate($base_field_definitions[$key], $settings, $result); + if ($base_field_property_value->getComponent('type') == $property_value->getComponent('type')) { + continue; + } + } + + $properties[$key] = $property_value; + } + + return $properties; + } + + protected function getMapping($object, Settings $settings, Result $result, ComponentResult $component_result) { + $properties = $this->getProperties($object, $settings, $result, $component_result); + + $entity_type_id = $this->getEntityId($object); + $entity_type = $this->entityTypeManager->getDefinition($entity_type_id); + + $entities = $result->getContext('entities'); + + if (!$entities || !isset($entities[$entity_type_id])) { + return NULL; + } + + $mapping = [ + $entities[$entity_type_id]->getContext('base'), + 'bundle' => $entity_type->getKey('bundle'), + ]; + + foreach ($properties as $key => $property) { + if (array_search($key, $mapping) === FALSE) { + $mapping[$key] = $key; + } + } + + return $mapping; + } + + public function generateTargetType($object, Settings $settings, Result $result, ComponentResult $componentResult) { + /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $object */ + return $componentResult->getContext('internal')->getComponent('target_type'); + } + + public function generateParser($object, Settings $settings, Result $result, ComponentResult $componentResult) { + /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $object */ + return $componentResult->getContext('internal')->getComponent('parser'); + } + + public function generateType($object, Settings $settings, Result $result, ComponentResult $componentResult) { + /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $object */ + return $componentResult->getContext('internal')->getComponent('type'); + } + + protected function generateInternal($object, Settings $settings, Result $result, ComponentResult $componentResult) { + $properties = $this->getProperties($object, $settings, $result, $componentResult); + $mapping = $this->getMapping($object, $settings, $result, $componentResult); + $name = $this->getName($object, $settings, $result, $componentResult); + + return $this->generatePropertiesComponentResult( + $properties, + $name, + 'Parsed' . $name, + Container::underscore($name) . '_parser', + $mapping, + $settings, + $result + ); + } + + protected function preGenerate($object, Settings $settings, Result $result, ComponentResult $componentResult) { + parent::preGenerate($object, $settings, $result, $componentResult); + + $internal = $componentResult->getContext('internal'); + if (!isset($internal)) { + $internal = $this->generateInternal($object, $settings, $result, $componentResult); + $componentResult->setContext('internal', $internal); + } + } + + public function generateGuard($object, Settings $settings, Result $result, ComponentResult $componentResult) { + /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $object */ + $name = Container::underscore($this->getName($object, $settings, $result, $componentResult)) . '_guard'; + + $entity_type_id = $this->getEntityId($object); + $entity_type = $this->entityTypeManager->getDefinition($entity_type_id); + + return $result->setComponent( + 'parser/' . $name, + 'const ' . $name . ' = (t: :types/' . Container::camelize($entity_type_id) . ':): t is ' . $componentResult->getComponent('type') . + " => :parser/" . $entity_type_id . '_bundle_parser:' . '(t.' . $entity_type->getKey('bundle') . ') == ' . json_encode($object->id()) + ); + } + + protected function postGenerate($object, Settings $settings, Result $result, ComponentResult $componentResult) { + /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $object */ + + if ($settings->generateParser()) { + $componentResult->setComponent('guard', $this->generateGuard($object, $settings, $result, $componentResult)); + } + } +} \ No newline at end of file diff --git a/src/ComponentGenerator/Entity/EntityGenerator.php b/src/ComponentGenerator/Entity/EntityGenerator.php new file mode 100644 index 0000000..242c4b0 --- /dev/null +++ b/src/ComponentGenerator/Entity/EntityGenerator.php @@ -0,0 +1,335 @@ +entityTypeManager = $entityTypeManager; + $this->entityFieldManager = $entityFieldManager; + } + + public function supportsGeneration($object) { + return $object instanceof EntityTypeInterface; + } + + /** + * @param $object + * @return array|bool + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + */ + protected function getBundles($object) { + /** @var \Drupal\Core\Entity\EntityTypeInterface $object */ + if ($bundle_entity_type = $object->getBundleEntityType()) { + $bundles = []; + foreach ($this->entityTypeManager->getStorage($bundle_entity_type)->loadMultiple() as $entity) { + $bundles[] = $entity; + } + + return $bundles; + } + + return FALSE; + } + + protected function getBaseName($object) { + /** @var \Drupal\Core\Entity\EntityTypeInterface $object */ + return Container::camelize($object->id()) . "Base"; + } + + protected function getBaseMapping($object, array $properties, Settings $settings, Result $result, ComponentResult $componentResult) { + /** @var \Drupal\Core\Entity\EntityTypeInterface $object */ + $base_field_definitions = $this->entityFieldManager->getBaseFieldDefinitions($object->id()); + + $mapping = [ + 'entity_type' => new ComponentResult([ + 'target_type' => json_encode($object->id()), + 'parser' => '((_: any): ' . json_encode($object->id()) . ' => ' . json_encode($object->id()) . ')', + ]) + ]; + + $custom_mapped_keys = [ + 'id' => 'id', + 'status' => 'status', + 'uid' => 'author', + 'langcode' => 'language', + 'label' => 'label', + ]; + + foreach ($custom_mapped_keys as $key => $target_property_key) { + if ($property_key = $object->getKey($key)) { + $mapping[$target_property_key] = $property_key; + } + } + + $ignored_keys = array_filter([ + $object->getKey('revision'), + $object->getKey('uuid'), + $object->getKey('bundle'), + ]); + + foreach ($properties as $key => $property) { + if (array_search($key, $mapping) === FALSE && !in_array($key, $ignored_keys)) { + $mapping[$key] = $key; + } + } + + return $mapping; + } + + protected function getBaseProperties($object, Settings $settings, Result $result, ComponentResult $componentResult) { + /** @var \Drupal\Core\Entity\EntityTypeInterface $object */ + $base_field_definitions = $this->entityFieldManager->getBaseFieldDefinitions($object->id()); + + $properties = []; + foreach ($base_field_definitions as $key => $field_definition) { + if ($field_definition->isInternal()) { + continue; + } + $properties[$key] = $this->generator->generate($field_definition, $settings, $result); + + if ($key == $object->getKey('bundle')) { + $properties[$key] = clone $properties[$key]; + $properties[$key]->setComponent('target_type', 'string'); + $properties[$key]->setComponent('parser', $componentResult->setComponent('bundle_parser', $result->setComponent( + 'parser/' . $object->id() . '_bundle_parser', + 'const ' . $object->id() . '_bundle_parser = (f: ' . $properties[$key]->getComponent('type') . "): string => f[0].target_id" + ))); + } + } + + return $properties; + } + + protected function generateBase($object, Settings $settings, Result $result, ComponentResult $componentResult) { + $properties = $this->getBaseProperties($object, $settings, $result, $componentResult); + + return $this->generatePropertiesComponentResult( + $properties, + $this->getBaseName($object), + 'Parsed' . $this->getBaseName($object), + Container::underscore($this->getBaseName($object)) . '_parser', + $this->getBaseMapping($object, $properties, $settings, $result, $componentResult), + $settings, + $result + ); + } + + /** + * @param $object + * @param Settings $settings + * @param Result $result + * @param ComponentResult $componentResult + * @return array|bool + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + */ + public function generateBundles($object, Settings $settings, Result $result, ComponentResult $componentResult) { + $bundle_list = $this->getBundles($object); + if (!$bundle_list) { + return FALSE; + } + + $bundles = []; + foreach ($bundle_list as $bundle) { + $bundles[$bundle->id()] = $this->generator->generate($bundle, $settings, $result); + } + return $bundles; + } + + public function preGenerateBundles($object, Settings $settings, Result $result, ComponentResult $componentResult) { + $entity_type_id = $object->id(); + $bundle_list = $this->getBundles($object); + if (!$bundle_list) { + return FALSE; + } + + $bundles = []; + foreach ($bundle_list as $bundle) { + $name = Container::camelize($entity_type_id) . Container::camelize($bundle->id()); + $bundles[$bundle->id()] = new ComponentResult([ + 'type' => ':types/' . $name . ':', + 'target_type' => ':types/Parsed' . $name . ':', + 'parser' => ':parser/' . Container::underscore($name) . '_parser:', + 'guard' => ':parser/' . Container::underscore($name) . '_guard:', + ]); + } + return $bundles; + } + + public function generateTargetType($object, Settings $settings, Result $result, ComponentResult $componentResult) { + /** @var \Drupal\Core\Entity\EntityTypeInterface $object */ + + $name = 'Parsed' . Container::camelize($object->id()); + $bundles = $componentResult->getContext('bundles'); + + if ($bundles) { + /** @var ComponentResult[] $bundles */ + return $result->setComponent('types/' . $name, 'type ' . $name . " = " . $this->generateUnionObject($bundles, 'target_type')); + } else { + $baseResult = $componentResult->getContext('base'); + + return $result->setComponent('types/' . $name, 'type ' . $name . ' = ' . $baseResult->getComponent('target_type')); + } + } + + public function generateParser($object, Settings $settings, Result $result, ComponentResult $componentResult) { + /** @var \Drupal\Core\Entity\EntityTypeInterface $object */ + $name = $object->id() . '_parser'; + $bundles = $componentResult->getContext('bundles'); + + if ($bundles) { + /** @var ComponentResult[] $bundles */ + return $result->setComponent( + 'parser/' . $name, + 'const ' . $name . ' = ' . $this->generateUnionParser($bundles, $componentResult->getComponent('type'), $componentResult->getComponent('target_type')) + ); + } else { + $baseResult = $componentResult->getContext('base'); + + return $result->setComponent( + 'parser/' . $name, + 'const ' . $name . ' = (t: ' . $componentResult->getComponent('type') . '): ' . $componentResult->getComponent('target_type') . " => " . $baseResult->getComponent('parser') . '(t)' + ); + } + } + + public function generateType($object, Settings $settings, Result $result, ComponentResult $componentResult) { + /** @var \Drupal\Core\Entity\EntityTypeInterface $object */ + $name = Container::camelize($object->id()); + $bundles = $componentResult->getContext('bundles'); + + if ($bundles) { + /** @var ComponentResult[] $bundles */ + return $result->setComponent('types/' . $name, 'type ' . $name . " = " . $this->generateUnionObject($bundles, 'type')); + } else { + $baseResult = $componentResult->getContext('base'); + + return $result->setComponent('types/' . $name, 'type ' . $name . ' = ' . $baseResult->getComponent('type')); + } + } + + public function generateGuard($object, Settings $settings, Result $result, ComponentResult $componentResult) { + /** @var \Drupal\Core\Entity\EntityTypeInterface $object */ + $name = $name = $object->id() . '_guard'; + $base_field_definitions = $this->entityFieldManager->getBaseFieldDefinitions($object->id()); + + $conditions = []; + foreach ($base_field_definitions as $key => $field_definition) { + if ($field_definition->isInternal() || $field_definition->getType() == 'path') { + continue; + } + $conditions[] = ' (t as any).' . $key . ' !== undefined'; + + if ($key == $object->getKey('bundle') && ($bundle_entity_type = $object->getBundleEntityType())) { + $conditions[] = ' (t as any).' . $key .'[0].target_type == ' . json_encode($bundle_entity_type); + } + } + + return $result->setComponent( + 'parser/' . $name, + 'const ' . $name . ' = (t: :types/Entity:): t is ' . $componentResult->getComponent('type') . + " => (\n" . implode(" &&\n", $conditions) . "\n)" + ); + } + + /** + * @param $object + * @param Settings $settings + * @param Result $result + * @param ComponentResult $componentResult + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + */ + protected function preGenerate($object, Settings $settings, Result $result, ComponentResult $componentResult) { + /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $object */ + parent::preGenerate($object, $settings, $result, $componentResult); + + if (!$componentResult->hasComponent('type')) { + $name = Container::camelize($object->id()); + $componentResult->setComponent('type', ':types/' . $name . ':'); + } + + if ($settings->generateParser()) { + if (!$componentResult->hasComponent('target_type')) { + $name = 'Parsed' . Container::camelize($object->id()); + $componentResult->setComponent('target_type', ':types/' . $name . ':'); + } + if (!$componentResult->hasComponent('parser')) { + $name = $name = $object->id() . '_parser'; + $componentResult->setComponent('parser', ':parser/' . $name . ':'); + } + if (!$componentResult->hasComponent('guard')) { + $name = $name = $object->id() . '_guard'; + $componentResult->setComponent('guard', ':parser/' . $name . ':'); + } + } + + $base = $componentResult->getContext('base'); + if (!isset($base)) { + $base = $this->generateBase($object, $settings, $result, $componentResult); + $componentResult->setContext('base', $base); + } + + $bundles = $componentResult->getContext('bundles'); + if (!isset($bundles)) { + $bundles = $this->preGenerateBundles($object, $settings, $result, $componentResult); + $componentResult->setContext('bundles', $bundles); + $bundles = $this->generateBundles($object, $settings, $result, $componentResult); + $componentResult->setContext('bundles', $bundles); + } + } + + protected function postGenerate($object, Settings $settings, Result $result, ComponentResult $componentResult) { + /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $object */ + parent::postGenerate($object, $settings, $result, $componentResult); + + if ($settings->generateParser()) { + $componentResult->setComponent('guard', $this->generateGuard($object, $settings, $result, $componentResult)); + } + } + + public function generate($object, Settings $settings, Result $result) { + /** @var \Drupal\Core\Entity\EntityTypeInterface $object */ + + $componentResult = new ComponentResult(); + + $name = $object->id(); + $context = $result->getContext('entities'); + if (!isset($context)) { + $context = []; + } + + if (isset($context[$name])) { + return $context[$name]; + } + + $context[$name] = $componentResult; + $result->setContext('entities', $context); + + $this->generateWithComponentResult($object, $settings, $result, $componentResult); + + return $componentResult; + } +} \ No newline at end of file diff --git a/src/ComponentGenerator/Field/EntityReferenceFieldGenerator.php b/src/ComponentGenerator/Field/EntityReferenceFieldGenerator.php new file mode 100644 index 0000000..3a6b8b3 --- /dev/null +++ b/src/ComponentGenerator/Field/EntityReferenceFieldGenerator.php @@ -0,0 +1,79 @@ +entityTypeManager = $entityTypeManager; + } + + protected function getCoreName($object) { + return Container::camelize($object->getType()); + } + + protected function getName($object, Settings $settings, Result $result, ComponentResult $component_result) { + return $this->getCoreName($object) . Container::camelize($object->getSettings()['target_type']); + } + + protected function getItemProperties($object, Settings $settings, Result $result, ComponentResult $component_result) { + $properties = parent::getItemProperties($object, $settings, $result, $component_result); + + $properties['target_type'] = 'string'; + $properties['target_uuid'] = 'string'; + + if ($this->hasUrl($object)) { + $properties['url'] = 'string'; + } + + return $properties; + } + + public function getItemMapping($object, $properties, Settings $settings, Result $result, ComponentResult $componentResult) { + if (isset($properties['url'])) { + return ['id' => 'target_id', 'url']; + } else { + return 'target_id'; + } + // return parent::getItemMapping($object, $properties, $settings, $result, $componentResult); // TODO: Change the autogenerated stub + } + + protected function hasUrl($object) { + /** @var \Drupal\Core\Field\FieldDefinitionInterface $object */ + if ($object->getSettings()['target_type'] == 'file') { + return TRUE; + } + + $entity_type = $this->entityTypeManager->getDefinition($object->getSettings()['target_type']); + return $entity_type->hasLinkTemplate('canonical'); + } + + public function generateType($object, Settings $settings, Result $result, ComponentResult $componentResult) { + /** @var \Drupal\Core\Field\FieldDefinitionInterface $object */ + $type = parent::generateType($object, $settings, $result, $componentResult); + + $entity_type = $this->entityTypeManager->getDefinition($object->getSettings()['target_type']); + $name = $this->getItemName($object, $settings, $result, $componentResult); + if ($entity_type instanceof ConfigEntityTypeInterface) { + $componentResult->setComponent('specific_item_type', $result->setComponent('types/' . $name . 'Specific', "type " . $name . "Specific = " . $componentResult->getContext('item')->getComponent('type') . " & {\n target_id: T,\n}")); + } + + return $type; + } +} \ No newline at end of file diff --git a/src/ComponentGenerator/Field/FieldGenerator.php b/src/ComponentGenerator/Field/FieldGenerator.php new file mode 100644 index 0000000..b042e2b --- /dev/null +++ b/src/ComponentGenerator/Field/FieldGenerator.php @@ -0,0 +1,146 @@ +fieldTypePluginManager = $fieldTypePluginManager; + } + + public function supportsGeneration($object) { + if (!($object instanceof FieldDefinitionInterface)) { + return FALSE; + } + + $supported = (array) $this->supportedFieldType; + + return !isset($this->supportedFieldType) || in_array($object->getType(), $supported); + } + + protected function getName($object, Settings $settings, Result $result, ComponentResult $component_result) { + /** @var \Drupal\Core\Field\FieldDefinitionInterface $object */ + return Container::camelize($object->getType()); + } + + protected function getItemName($object, Settings $settings, Result $result, ComponentResult $component_result) { + /** @var \Drupal\Core\Field\FieldDefinitionInterface $object */ + return $this->getName($object, $settings, $result, $component_result) . 'Item'; + } + + protected function getItemProperties($object, Settings $settings, Result $result, ComponentResult $component_result) { + /** @var \Drupal\Core\Field\FieldDefinitionInterface $object */ + $storage_object = $object->getFieldStorageDefinition(); + + $properties = []; + foreach ($storage_object->getPropertyDefinitions() as $key => $property) { + if ($property->isInternal()) { + continue; + } + + $properties[$key] = $this->generator->generate($property, $settings, $result); + } + + return $properties; + } + + public function getItemMapping($object, $properties, Settings $settings, Result $result, ComponentResult $componentResult) { + if (count($properties) == 1) { + $property_keys = array_keys($properties); + return $property_keys[0]; + } + + return NULL; + } + + protected function generateItem($object, Settings $settings, Result $result, ComponentResult $componentResult) { + $name = $this->getItemName($object, $settings, $result, $componentResult); + + $properties = $this->getItemProperties($object, $settings, $result, $componentResult); + $mapping = $this->getItemMapping($object, $properties, $settings, $result, $componentResult); + + return $this->generatePropertiesComponentResult( + $properties, + $name, + 'Parsed' . $name, + Container::underscore($name) . '_parser', + $mapping, + $settings, + $result + ); + } + + protected function generateWrapperType($object, Settings $settings, Result $result, ComponentResult $componentResult) { + /** @var \Drupal\Core\Field\FieldDefinitionInterface $object */ + return $result->setComponent('types/FieldItemList', "type FieldItemList = T[]"); + } + + public function generateType($object, Settings $settings, Result $result, ComponentResult $componentResult) { + /** @var \Drupal\Core\Field\FieldDefinitionInterface $object */ + $componentResult->setComponent('wrapper_type', $this->generateWrapperType($object, $settings, $result, $componentResult)); + return $componentResult->getComponent('wrapper_type') . '<' . $componentResult->getContext('item')->getComponent('type') . '>'; + } + + public function generateTargetType($object, Settings $settings, Result $result, ComponentResult $componentResult) { + /** @var \Drupal\Core\Field\FieldDefinitionInterface $object */ + + $item_target_type = $componentResult->getContext('item')->getComponent('target_type'); + if ($object->getFieldStorageDefinition()->getCardinality() == 1) { + return $object->isRequired() ? $item_target_type : ($item_target_type . ' | null'); + } else { + return ':/immutable/List:<' . $item_target_type . '>'; + } + } + + public function generateParser($object, Settings $settings, Result $result, ComponentResult $componentResult) { + /** @var \Drupal\Core\Field\FieldDefinitionInterface $object */ + + $type = $this->generateType($object, $settings, $result, $componentResult); + $target_type = $this->generateTargetType($object, $settings, $result, $componentResult); + $target_type = $this->cleanupPropertyType($target_type); + + $item_target_type = $componentResult->getContext('item')->getComponent('target_type'); + $item_parser = $componentResult->getContext('item')->getComponent('parser'); + + if ($object->getFieldStorageDefinition()->getCardinality() == 1) { + if ($object->isRequired()) { + $name = 'singular_required_' . Container::underscore($this->getName($object, $settings, $result, $componentResult)) . '_parser'; + return $result->setComponent('parser/' . $name, 'const ' . $name . ' = (f: ' . $type . '): ' . $target_type . ' => ' . $item_parser . '(f[0])'); + } else { + $name = 'singular_optional_' . Container::underscore($this->getName($object, $settings, $result, $componentResult)) . '_parser'; + return $result->setComponent('parser/' . $name, 'const ' . $name . ' = (f: ' . $type . '): ' . $target_type . ' => f && f.length > 0 ? ' . $item_parser . '(f[0]) : null'); + } + } else { + // parse from FieldItemList to Immutable.List + $name = 'plural_' . Container::underscore($this->getName($object, $settings, $result, $componentResult)) . '_parser'; + return $result->setComponent('parser/' . $name, 'const ' . $name . " =\n (f: " . $type . '): ' . $target_type . " =>\n :/immutable/List:<" . $item_target_type . '>(f.map(i => ' . $item_parser . '(i)))'); + } + } + + protected function preGenerate($object, Settings $settings, Result $result, ComponentResult $componentResult) { + parent::preGenerate($object, $settings, $result, $componentResult); + + $item = $componentResult->getContext('item'); + if (!isset($item)) { + $item = $this->generateItem($object, $settings, $result, $componentResult); + $componentResult->setContext('item', $item); + } + } +} \ No newline at end of file diff --git a/src/ComponentGenerator/Field/LanguageFieldGenerator.php b/src/ComponentGenerator/Field/LanguageFieldGenerator.php new file mode 100644 index 0000000..87882f1 --- /dev/null +++ b/src/ComponentGenerator/Field/LanguageFieldGenerator.php @@ -0,0 +1,38 @@ +languageManager = $languageManager; + } + + protected function generateLanguageObject($object, Settings $settings, Result $result) { + return $this->generator->generate($this->languageManager, $settings, $result)->getComponent('type'); + } + + protected function getItemProperties($object, Settings $settings, Result $result, ComponentResult $componentResult) { + $properties = parent::getItemProperties($object, $settings, $result, $componentResult); + + $properties['value'] = $this->generateLanguageObject($object, $settings, $result); + $componentResult->setComponent('language_type', $properties['value']); + + return $properties; + } +} \ No newline at end of file diff --git a/src/ComponentGenerator/Field/PathFieldGenerator.php b/src/ComponentGenerator/Field/PathFieldGenerator.php new file mode 100644 index 0000000..90e30a5 --- /dev/null +++ b/src/ComponentGenerator/Field/PathFieldGenerator.php @@ -0,0 +1,19 @@ +getComponent('type'); + } + + /** + * @param $object + * @param \Drupal\ts_generator\Settings $settings + * @param \Drupal\ts_generator\Result $result + * @param \Drupal\ts_generator\ComponentResultInterface $componentResult + * @return string + */ + protected function generateParser($object, Settings $settings, Result $result, ComponentResult $componentResult) { + return $this->generateNoopParser($settings, $result, $componentResult) . '<' . $componentResult->getComponent('type') . '>'; + } + + protected function preGenerate($object, Settings $settings, Result $result, ComponentResult $componentResult) {} + protected function postGenerate($object, Settings $settings, Result $result, ComponentResult $componentResult) {} + + protected function generateWithComponentResult($object, Settings $settings, Result $result, ComponentResult $componentResult) { + $this->preGenerate($object, $settings, $result, $componentResult); + + $componentResult->setComponent('type', $this->generateType($object, $settings, $result, $componentResult)); + if ($settings->generateParser()) { + $componentResult->setComponent('target_type', $this->generateTargetType($object, $settings, $result, $componentResult)); + $componentResult->setComponent('parser', $this->generateParser($object, $settings, $result, $componentResult)); + } + + $this->postGenerate($object, $settings, $result, $componentResult); + } + + /** + * @param $object + * @param \Drupal\ts_generator\Settings $settings + * @param \Drupal\ts_generator\Result $result + * @return \Drupal\ts_generator\ComponentResultInterface + */ + public function generate($object, Settings $settings, Result $result) { + $componentResult = new ComponentResult(); + + $this->generateWithComponentResult($object, $settings, $result, $componentResult); + + return $componentResult; + } +} \ No newline at end of file diff --git a/src/ComponentGenerator/Manager/EntityTypeManagerGenerator.php b/src/ComponentGenerator/Manager/EntityTypeManagerGenerator.php new file mode 100644 index 0000000..64c1637 --- /dev/null +++ b/src/ComponentGenerator/Manager/EntityTypeManagerGenerator.php @@ -0,0 +1,124 @@ +getEntities(); + foreach ($entity_ids as $entity_id) { + $entity_type = $object->getDefinition($entity_id); + $this->generator->generate($entity_type, $settings, $result); + } + } + + protected function generateType($object, Settings $settings, Result $result, ComponentResult $componentResult) { + /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $object */ + + /** @var \Drupal\ts_generator\ComponentResult[] $entities */ + $entities = $result->getContext('entities'); + + $input_entities = []; + foreach ($settings->getInputEntities() as $entity_id) { + $input_entities[$entity_id] = $entities[$entity_id]; + } + + $componentResult->setComponent( + 'input_type', + $result->setComponent('types/InputEntity', "type InputEntity = " . $this->generateUnionObject($input_entities, 'type')) + ); + + return $result->setComponent('types/Entity', "type Entity = " . $this->generateUnionObject($entities, 'type')); + } + + protected function generateTargetType($object, Settings $settings, Result $result, ComponentResult $componentResult) { + /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $object */ + + /** @var \Drupal\ts_generator\ComponentResult[] $entities */ + $entities = $result->getContext('entities'); + + $input_entities = []; + foreach ($settings->getInputEntities() as $entity_id) { + $input_entities[$entity_id] = $entities[$entity_id]; + } + + $componentResult->setComponent( + 'input_target_type', + $result->setComponent('types/ParsedInputEntity', "type ParsedInputEntity = " . $this->generateUnionObject($input_entities, 'target_type')) + ); + + return $result->setComponent('types/ParsedEntity', "type ParsedEntity = " . $this->generateUnionObject($entities, 'target_type')); + } + + protected function generateParser($object, Settings $settings, Result $result, ComponentResult $componentResult) { + $entities = $result->getContext('entities'); + + $input_entities = []; + foreach ($settings->getInputEntities() as $entity_id) { + $input_entities[$entity_id] = $entities[$entity_id]; + } + + $componentResult->setComponent( + 'input_parser', + $result->setComponent( + 'parser/input_entity_parser', + 'const input_entity_parser = ' . $this->generateUnionParser($input_entities, $componentResult->getComponent('input_type'), $componentResult->getComponent('input_target_type')) + ) + ); + + return $result->setComponent( + 'parser/entity_parser', + 'const entity_parser = ' . $this->generateUnionParser($entities, $componentResult->getComponent('type'), $componentResult->getComponent('target_type')) + ); + } + + public function generateGuard($object, Settings $settings, Result $result, ComponentResult $componentResult) { + $entities = $result->getContext('entities'); + + $input_entities = []; + foreach ($settings->getInputEntities() as $entity_id) { + $input_entities[$entity_id] = $entities[$entity_id]; + } + + $componentResult->setComponent( + 'input_guard', + $result->setComponent( + 'parser/input_entity_guard', + 'const input_entity_guard = ' . $this->generateUnionGuard($input_entities, 'any', $componentResult->getComponent('input_type'), $componentResult->getComponent('input_type')) + ) + ); + + return $result->setComponent( + 'parser/entity_guard', + 'const entity_guard = ' . $this->generateUnionGuard($entities, 'any', $componentResult->getComponent('type'), $componentResult->getComponent('type')) + ); + } + + protected function preGenerate($object, Settings $settings, Result $result, ComponentResult $componentResult) { + $this->generateEntities($object, $settings, $result, $componentResult); + } + + protected function postGenerate($object, Settings $settings, Result $result, ComponentResult $componentResult) { + /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $object */ + parent::postGenerate($object, $settings, $result, $componentResult); + + if ($settings->generateParser()) { + $componentResult->setComponent('guard', $this->generateGuard($object, $settings, $result, $componentResult)); + } + } + + public function supportsGeneration($object) { + return ($object instanceof EntityTypeManagerInterface); + } + +} \ No newline at end of file diff --git a/src/ComponentGenerator/Manager/LanguageManagerGenerator.php b/src/ComponentGenerator/Manager/LanguageManagerGenerator.php new file mode 100644 index 0000000..809059d --- /dev/null +++ b/src/ComponentGenerator/Manager/LanguageManagerGenerator.php @@ -0,0 +1,29 @@ +getLanguages(); + $langcodes = []; + foreach ($languages as $language) { + $langcodes[] = '\'' . $language->getId() . '\''; + } + + return $result->setComponent('types/Language', "type Language = " . implode(' | ', $langcodes)); + } + + public function supportsGeneration($object) { + return ($object instanceof LanguageManagerInterface); + } +} \ No newline at end of file diff --git a/src/ComponentGenerator/NoopParserGenerator.php b/src/ComponentGenerator/NoopParserGenerator.php new file mode 100644 index 0000000..cad14d0 --- /dev/null +++ b/src/ComponentGenerator/NoopParserGenerator.php @@ -0,0 +1,19 @@ +setComponent('parser/noop_parser', 'const noop_parser = (t: T): T => t'); + } +} \ No newline at end of file diff --git a/src/ComponentGenerator/PropertiesGenerator.php b/src/ComponentGenerator/PropertiesGenerator.php new file mode 100644 index 0000000..d799b08 --- /dev/null +++ b/src/ComponentGenerator/PropertiesGenerator.php @@ -0,0 +1,246 @@ +getDefaultMapping($properties); + } + + $combinators = []; + + if (!is_string($mapping)) { + foreach ($mapping as $property_target_key => $property_key) { + if (!is_numeric($property_target_key)) { + continue; + } + + if (is_string($property_key) && isset($properties[$property_key])) { + continue; + } + + if (is_string($property_key)) { + $combinators[] = $property_key; + } else { + $combinators[] = $property_key->getComponent('type'); + } + } + } + + $result_properties = []; + foreach ($properties as $key => $property) { + $result_properties[$key] = $this->generatePropertyType($properties, $key, 'type'); + } + + return $this->formatObject($combinators, $result_properties); + } + + protected function formatObject($combinators, $result_properties) { + $_result_properties = []; + + foreach ($result_properties as $key => $value) { + $optional = FALSE; + if (substr($key, -1, 1) == '?') { + $key = substr($key, 0, -1); + $optional = TRUE; + } + + if (!preg_match('/^[a-zA-Z_$][0-9a-zA-Z_$]*$/', $key)) { + $key = json_encode($key); + } + + $_result_properties[] = ' ' . $key . ($optional ? '?' : '') . ': ' . $value; + } + + return ($combinators || $_result_properties) ? implode(' & ' , array_filter([ + ($combinators ? (implode(' & ', $combinators)) : ''), + ($_result_properties ? "{\n" . implode(",\n", $_result_properties) . "\n}" : '') + ])) : '{}'; + } + + protected function generatePropertyType(array $properties, $property_key, $component = 'type') { + $property_data = is_string($properties[$property_key]) ? $properties[$property_key] : $properties[$property_key]->getComponent($component); + $property_data = $this->cleanupPropertyType($property_data); + return $property_data; + } + + protected function getDefaultMapping(array $properties, $mapping = NULL) { + return array_combine(array_keys($properties), array_keys($properties)); + } + + protected function generatePropertiesTargetObject(array $properties, $mapping = NULL) { + if (!isset($mapping)) { + $mapping = $this->getDefaultMapping($properties); + } + + if (is_string($mapping)) { + if (isset($properties[$mapping])) { + return $this->generatePropertyType($properties, $mapping, 'target_type'); + } else { + $mapping = [$mapping]; + } + } + + $result = []; + $combinators = []; + + foreach ($mapping as $property_target_key => $property_key) { + if (is_numeric($property_target_key)) { + if (is_string($property_key) && isset($properties[$property_key])) { + $result[$property_key] = $this->generatePropertyType($properties, $property_key, 'target_type'); + } else { + if (is_string($property_key)) { + $combinators[] = $property_key; + } else { + $combinators[] = $property_key->getComponent('target_type'); + } + } + } else { + if (is_string($property_key) && isset($properties[$property_key])) { + $result[$property_target_key] = $this->generatePropertyType($properties, $property_key, 'target_type'); + } else { + if (is_string($property_key)) { + $result[$property_target_key] = $property_key; + } else { + $result[$property_target_key] = $property_key->getComponent('target_type'); + } + } + } + } + + return $this->formatObject($combinators, $result); + } + + protected function cleanupPropertyType($type) { + $_type = explode(' | ', $type); + $_uniq_type = array_unique($_type); + + return implode(' | ', $_uniq_type); + } + + protected function generatePropertyParser(array $properties, $property_key) { + if (is_string($properties[$property_key])) { + if (!preg_match('/^[a-zA-Z_$][0-9a-zA-Z_$]*$/', $property_key)) { + return 't[' . json_encode($property_key) . ']'; + } else { + return 't.' . $property_key; + } + } else { + if (!preg_match('/^[a-zA-Z_$][0-9a-zA-Z_$]*$/', $property_key)) { + return $properties[$property_key]->getComponent('parser') . '(t[' . json_encode($property_key) . '])'; + } else { + return $properties[$property_key]->getComponent('parser') . '(t.' . $property_key . ')'; + } + } + } + + protected function generatePropertyParserLine($key, $value) { + if (!preg_match('/^[a-zA-Z_$][0-9a-zA-Z_$]*$/', $key)) { + $key = json_encode($key); + } + + return ' ' . $key . ': ' . $value; + } + + /** + * @param \Drupal\ts_generator\ComponentResultInterface|string[] $properties + * @param null|string|array $mapping + * @param string $type_name + * @param string $target_type_name + * @return string + */ + protected function generatePropertiesParser(array $properties, $type_name, $target_type_name, $mapping = NULL) { + return '(t: ' . $type_name . '): ' . $target_type_name . " => " . $this->generatePropertiesParserContent($properties, $mapping); + } + + protected function generatePropertiesParserContent(array $properties, $mapping = NULL) { + if (!isset($mapping)) { + $mapping = $this->getDefaultMapping($properties); + } + + if (is_string($mapping)) { + if (isset($properties[$mapping])) { + return $this->generatePropertyParser($properties, $mapping); + } else { + $mapping = [$mapping]; + } + } + + $result = []; + + foreach ($mapping as $property_target_key => $property_key) { + if (is_numeric($property_target_key)) { + if (is_string($property_key) && isset($properties[$property_key])) { + $result[] = $this->generatePropertyParserLine($property_key, $this->generatePropertyParser($properties, $property_key)); + } else { + if (is_string($property_key)) { + $result[] = ' ...' . $property_key . '(t)'; + } else { + $result[] = ' ...' . $property_key->getComponent('parser') . '(t)'; + } + } + } else { + if (is_string($property_key) && isset($properties[$property_key])) { + $result[] = $this->generatePropertyParserLine($property_target_key, $this->generatePropertyParser($properties, $property_key)); + } else { + if (is_string($property_key)) { + $result[] = $this->generatePropertyParserLine($property_target_key, $property_key . '(t)'); + } else { + $result[] = $this->generatePropertyParserLine($property_target_key, $property_key->getComponent('parser') . '(t)'); + } + } + } + } + + return "({\n" . implode(",\n", $result) . "\n})"; + } + + protected function generatePropertiesComponentResult($properties, $type_name, $target_type_name, $parser_name, $mapping, Settings $settings, Result $result) { + if (!isset($mapping)) { + $mapping = $this->getDefaultMapping($properties); + } + + $componentResult = new ComponentResult(); + + $type = $componentResult->setComponent( + 'type', + $result->setComponent( + 'types/' . $type_name, + 'type ' . $type_name . ' = ' . $this->generatePropertiesObject($properties, $mapping) + ) + ); + + if ($settings->generateParser()) { + $target_type = $componentResult->setComponent( + 'target_type', + !is_string($mapping) ? $result->setComponent( + 'types/' . $target_type_name, + 'type ' . $target_type_name . ' = ' . $this->generatePropertiesTargetObject($properties, $mapping) + ) : $this->generatePropertiesTargetObject($properties, $mapping) + ); + + $parser = $componentResult->setComponent( + 'parser', + $result->setComponent( + 'parser/' . $parser_name, + 'const ' . $parser_name . ' = ' . $this->generatePropertiesParser($properties, $type, $target_type, $mapping) + ) + ); + } + + return $componentResult; + } +} diff --git a/src/ComponentGenerator/UnionGenerator.php b/src/ComponentGenerator/UnionGenerator.php new file mode 100644 index 0000000..9190df2 --- /dev/null +++ b/src/ComponentGenerator/UnionGenerator.php @@ -0,0 +1,43 @@ +getComponent($component); + } + + return $single_line ? implode(' | ' , $_types) : ( + "(\n " . implode(" |\n ", $_types) . "\n)" + ); + } + + protected function generateUnionParser($types, $type_name, $target_type_name, $single_line = FALSE) { + $parserComponents = []; + foreach ($types as $type) { + $parserComponents[] = ($single_line ? '' : ' ') . $type->getComponent('guard') . '(t)'; + $parserComponents[] = ($single_line ? '' : ' ') . $type->getComponent('parser') . '(t)'; + } + + array_splice($parserComponents, -2 , 1); + + $parser = implode($single_line ? ' : ' : " :\n", array_map(function($a) use ($single_line) { + return implode($single_line ? ' ? ' : " ?\n", $a); + }, array_chunk($parserComponents, 2))); + + return '(t: ' . $type_name . '): ' . $target_type_name . ($single_line ? " => " : " =>\n") . $parser; + } + + protected function generateUnionGuard($types, $type_name, $guarded_type_name, $enforced_type_name = NULL) { + $guardComponents = []; + foreach ($types as $type) { + $guardComponents[] = ' ' . $type->getComponent('guard') . '(t' . ($enforced_type_name ? (' as ' . $enforced_type_name) : '') . ')'; + } + + return '(t: ' . $type_name . '): t is ' . $guarded_type_name . " => (\n" . implode(" ||\n", $guardComponents) . "\n)"; + } +} \ No newline at end of file diff --git a/src/ComponentResult.php b/src/ComponentResult.php new file mode 100644 index 0000000..0fcb4ed --- /dev/null +++ b/src/ComponentResult.php @@ -0,0 +1,46 @@ +components = $components; + $this->context = $context; + } + + /** + * @param $key string + * @param $component string + */ + public function setComponent(string $key, string $component) { + $this->components[$key] = $component; + return $component; + } + + public function hasComponent(string $key) { + return isset($this->components[$key]); + } + + /** + * @param $key + * @return bool|string + */ + public function getComponent(string $key) { + return isset($this->components[$key]) ? $this->components[$key] : FALSE; + } + + + public function getContext(string $key) { + return isset($this->context[$key]) ? $this->context[$key] : NULL; + } + + public function setContext(string $key, $value) { + $this->context[$key] = $value; + } +} \ No newline at end of file diff --git a/src/ComponentResultInterface.php b/src/ComponentResultInterface.php new file mode 100644 index 0000000..020717b --- /dev/null +++ b/src/ComponentResultInterface.php @@ -0,0 +1,26 @@ +setGenerator($this); + } + } + $this->component_generators = $component_generators; + } + + public function generate($object, Settings $settings, Result $result) { + if ($generator = $this->getGenerator($object)) { + return $generator->generate($object, $settings, $result); + } + + throw new \Exception("No supported Generator for " . var_export($object, TRUE)); + } + + public function supportsGeneration($object) { + $generator = $this->getGenerator($object); + return !empty($generator); + } + + private function getGenerator($object) { + foreach ($this->component_generators as $generator) { + if ($generator instanceof GeneratorInterface && $generator->supportsGeneration($object)) { + return $generator; + } + } + + return FALSE; + } +} \ No newline at end of file diff --git a/src/GeneratorAwareInterface.php b/src/GeneratorAwareInterface.php new file mode 100644 index 0000000..de139e7 --- /dev/null +++ b/src/GeneratorAwareInterface.php @@ -0,0 +1,8 @@ +generator = $generator; + } +} \ No newline at end of file diff --git a/src/GeneratorInterface.php b/src/GeneratorInterface.php new file mode 100644 index 0000000..539799e --- /dev/null +++ b/src/GeneratorInterface.php @@ -0,0 +1,19 @@ +getDefinition('ts_generator.generator'); + + $component_generators = []; + foreach ($container->findTaggedServiceIds('ts_generator_component') as $id => $attributes) { + $priority = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0; + $component_generators[$priority][] = new Reference($id); + } + + // Add the registered Normalizers and Encoders to the Serializer. + if (!empty($component_generators)) { + $definition->replaceArgument(0, $this->sort($component_generators)); + } + } + + /** + * Sorts by priority. + * + * Order services from highest priority number to lowest (reverse sorting). + * + * @param array $services + * A nested array keyed on priority number. For each priority number, the + * value is an array of Symfony\Component\DependencyInjection\Reference + * objects, each a reference to a normalizer or encoder service. + * + * @return array + * A flattened array of Reference objects from $services, ordered from high + * to low priority. + */ + protected function sort($services) { + $sorted = []; + krsort($services); + + // Flatten the array. + foreach ($services as $a) { + $sorted = array_merge($sorted, $a); + } + + return $sorted; + } + +} diff --git a/src/Result.php b/src/Result.php new file mode 100644 index 0000000..09f8406 --- /dev/null +++ b/src/Result.php @@ -0,0 +1,189 @@ +components = []; + $this->context = []; + } + + public function setComponent($key, $definition) { + $this->components[$key] = $definition; + + return ':' . $key . ':'; + } + + public function getComponent($key) { + return $this->components[$key]; + } + + public function hasComponent($key) { + return isset($this->components[$key]); + } + + public function getComponents() { + return $this->components; + } + + public function getContext($key) { + return isset($this->context[$key]) ? $this->context[$key] : NULL; + } + + public function setContext($key, $value) { + $this->context[$key] = $value; + } + + private function groupedComponents() { + $groups = []; + + foreach ($this->getComponents() as $key => $component) { + $_key = explode('/', $key); + $name = array_pop($_key); + $group_name = implode('/', $_key); + + if (!isset($groups[$group_name])) { + $groups[$group_name] = []; + } + + $groups[$group_name][$name] = $component; + } + + return $groups; + } + + // a/b/c, a/b/e => ./d + // a/b/c, a/e => ../d + // a/b/c/d, a/e => ../../e + private function resolveFilename($from, $to) { + if (substr($to, 0, 1) == '/') { + return substr($to, 1); + } + + $i = 0; + + $_from = explode('/', $from); + $_to = explode('/', $to); + + while ($_from[$i] && $_to[$i] && $_from[$i] == $_to[$i]) { + $i++; + } + + if (count($_from) - $i <= 1) { + return './' . implode('/', array_slice($_to, $i)); + } + + return implode('/', array_fill(0, count($_from) - $i - 1, '..')) . '/' . implode('/', array_slice($_to, $i)); + } + + private function fileData() { + $groups = $this->groupedComponents(); + + $files = []; + foreach ($groups as $file_name => $group) { + $names = []; + + foreach (array_keys($group) as $_name) { + $names[$file_name . '/' . $_name] = $_name; + }; + + $file = [ + 'imports' => [], + 'components' => [], + ]; + + foreach ($group as $name => $component) { + $file['components'][$name] = preg_replace_callback('/:([a-zA-Z0-9_*]*\/[a-zA-Z0-9_\/*]+):/', function($matches) use ($file_name, &$file, &$names) { + $key = $matches[1]; + if (isset($names[$key])) { + return $names[$key]; + } + + $_key = explode('/', $key); + $_name = array_pop($_key); + $_file_name = implode('/', $_key); + + if ($_file_name == $file_name) { + return $_name; + } + + $resolved_file_name = $this->resolveFileName($file_name, $_file_name); + + if (!isset($file['imports'][$resolved_file_name])) { + $file['imports'][$resolved_file_name] = []; + } + + if (substr($_name, -1, 1) == '*') { + $target_name = substr($_name, 0, -1); + $_name = '*'; + } else { + $target_name = $_name; + } + + if (array_search($target_name, $names) !== FALSE) { + $target_name = str_replace('/', '_', $_file_name) . '_' . $target_name; + } + + $file['imports'][$resolved_file_name][$_name] = $target_name; + $names[$key] = $target_name; + + return $target_name; + }, $component); + } + + $files[$file_name] = $file; + } + + return $files; + } + + private function files() { + $file_data = $this->fileData(); + + $files = []; + foreach ($file_data as $file_name => $file) { + $imports = []; + foreach ($file['imports'] as $_file_name => $_imports) { + $_import_spec = []; + foreach ($_imports as $key => $target) { + if ($key == '*') { + $imports[] = "import * as " . $target . " from '" . $_file_name . '\''; + } elseif ($key == $target) { + $_import_spec[] = ' ' . $key; + } else { + $_import_spec[] = ' ' . $key . ' as ' . $target; + } + } + + if (!empty($_import_spec)) { + $imports[] = "import {\n" . implode(",\n", $_import_spec) . "\n} from '" . $_file_name . '\''; + } + } + + $components = []; + foreach ($file['components'] as $component) { + $components[] = 'export ' . $component; + } + + $files[$file_name] = ($imports ? (implode("\n", $imports) . "\n\n") : '') . implode("\n\n", $components); + } + + return $files; + } + + public function write($target) { + $files = $this->files(); + + foreach ($files as $file_name => $content) { + $target_file_name = $target . '/' . $file_name . '.ts'; + $target_directory = dirname($target_file_name); + if (!is_dir($target_directory)) { + mkdir($target_directory, 0777, TRUE); + } + file_put_contents($target_file_name, $content); + } + } +} \ No newline at end of file diff --git a/src/Settings.php b/src/Settings.php new file mode 100644 index 0000000..3a0e410 --- /dev/null +++ b/src/Settings.php @@ -0,0 +1,67 @@ +data = NestedArray::mergeDeep( + static::defaultData(), + $data + ); + } + + public static function loadFile($filename) { + $data = Yaml::parseFile($filename); + + return new static($data); + } + + public static function defaultData() { + return []; + } + + public function writeToFile($filename) { + $data = Yaml::dump($this->data); + file_put_contents($filename, $data); + } + + public function get($key) { + return $this->data[$key]; + } + + public function getEntities() { + if (!is_array($this->data['entities'])) { + return [$this->data['entities']]; + } + + if (!isset($this->data['entities']['input']) && !isset($this->data['entities']['extra'])) { + return $this->data['entities']; + } + + return array_merge( + isset($this->data['entities']['input']) ? $this->data['entities']['input'] : [], + isset($this->data['entities']['extra']) ? $this->data['entities']['extra'] : [] + ); + } + + public function getInputEntities() { + if (!is_array($this->data['entities'])) { + return [$this->data['entities']]; + } + + if (!isset($this->data['entities']['input']) && !isset($this->data['entities']['extra'])) { + return $this->data['entities']; + } + + return isset($this->data['entities']['input']) ? $this->data['entities']['input'] : []; + } + + public function generateParser() { + return !empty($this->data['generate_parser']); + } +} diff --git a/src/TsGeneratorServiceProvider.php b/src/TsGeneratorServiceProvider.php new file mode 100644 index 0000000..135c795 --- /dev/null +++ b/src/TsGeneratorServiceProvider.php @@ -0,0 +1,16 @@ +addCompilerPass(new RegisterGeneratorClassesCompilerPass()); + } +} \ No newline at end of file diff --git a/ts_generator.info.yml b/ts_generator.info.yml new file mode 100644 index 0000000..455f58f --- /dev/null +++ b/ts_generator.info.yml @@ -0,0 +1,7 @@ +name: TS Generator +type: module +description: Generate TypeScript code based on the REST Resources +package: Hoppinger +core: 8.x +dependencies: + - rest diff --git a/ts_generator.services.yml b/ts_generator.services.yml new file mode 100644 index 0000000..76c47f6 --- /dev/null +++ b/ts_generator.services.yml @@ -0,0 +1,85 @@ +services: + ts_generator.generator: + class: Drupal\ts_generator\Generator + arguments: [{ }] + ts_generator.component_generator.language_manager: + class: Drupal\ts_generator\ComponentGenerator\Manager\LanguageManagerGenerator + tags: + - { name: ts_generator_component } + ts_generator.component_generator.entity_type_manager: + class: Drupal\ts_generator\ComponentGenerator\Manager\EntityTypeManagerGenerator + tags: + - { name: ts_generator_component } + ts_generator.component_generator.entity: + class: Drupal\ts_generator\ComponentGenerator\Entity\EntityGenerator + tags: + - { name: ts_generator_component } + arguments: ['@entity_type.manager', '@entity_field.manager'] + ts_generator.component_generator.field.entity_bundle: + class: Drupal\ts_generator\ComponentGenerator\Entity\EntityBundleGenerator + tags: + - { name: ts_generator_component } + arguments: ['@entity_type.manager', '@entity_field.manager', '@entity_type.repository'] + ts_generator.component_generator.field: + class: Drupal\ts_generator\ComponentGenerator\Field\FieldGenerator + tags: + - { name: ts_generator_component } + arguments: ['@plugin.manager.field.field_type'] + + ts_generator.component_generator.data.string: + class: Drupal\ts_generator\ComponentGenerator\Data\StringGenerator + tags: + - { name: ts_generator_component, priority: 10 } + ts_generator.component_generator.data.number: + class: Drupal\ts_generator\ComponentGenerator\Data\NumberGenerator + tags: + - { name: ts_generator_component, priority: 10 } + ts_generator.component_generator.data.boolean: + class: Drupal\ts_generator\ComponentGenerator\Data\BooleanGenerator + tags: + - { name: ts_generator_component, priority: 10 } + ts_generator.component_generator.data.filter_format: + class: Drupal\ts_generator\ComponentGenerator\Data\FilterFormatGenerator + tags: + - { name: ts_generator_component, priority: 10 } + arguments: ['@entity_type.manager'] + ts_generator.component_generator.data.date_time: + class: Drupal\ts_generator\ComponentGenerator\Data\DateTimeGenerator + tags: + - { name: ts_generator_component, priority: 10 } + arguments: ['@entity_type.manager'] + ts_generator.component_generator.data.any: + class: Drupal\ts_generator\ComponentGenerator\Data\AnyGenerator + tags: + - { name: ts_generator_component } + + ts_generator.component_generator.field.language: + class: Drupal\ts_generator\ComponentGenerator\Field\LanguageFieldGenerator + tags: + - { name: ts_generator_component, priority: 10 } + arguments: ['@plugin.manager.field.field_type', '@language_manager'] + ts_generator.component_generator.field.timestamp: + class: Drupal\ts_generator\ComponentGenerator\Field\TimestampFieldGenerator + tags: + - { name: ts_generator_component, priority: 10 } + arguments: ['@plugin.manager.field.field_type'] + ts_generator.component_generator.field.string: + class: Drupal\ts_generator\ComponentGenerator\Field\StringFieldGenerator + tags: + - { name: ts_generator_component, priority: 10 } + arguments: ['@plugin.manager.field.field_type'] + ts_generator.component_generator.field.text: + class: Drupal\ts_generator\ComponentGenerator\Field\TextFieldGenerator + tags: + - { name: ts_generator_component, priority: 10 } + arguments: ['@plugin.manager.field.field_type'] + ts_generator.component_generator.field.path: + class: Drupal\ts_generator\ComponentGenerator\Field\PathFieldGenerator + tags: + - { name: ts_generator_component, priority: 10 } + arguments: ['@plugin.manager.field.field_type'] + ts_generator.component_generator.field.entity_reference: + class: Drupal\ts_generator\ComponentGenerator\Field\EntityReferenceFieldGenerator + tags: + - { name: ts_generator_component, priority: 10 } + arguments: ['@plugin.manager.field.field_type', '@entity_type.manager'] \ No newline at end of file diff --git a/ts_generator_metatag/src/ComponentGenerator/MetatagBaseFieldGenerator.php b/ts_generator_metatag/src/ComponentGenerator/MetatagBaseFieldGenerator.php new file mode 100644 index 0000000..57bf7a2 --- /dev/null +++ b/ts_generator_metatag/src/ComponentGenerator/MetatagBaseFieldGenerator.php @@ -0,0 +1,111 @@ +metatagTagPluginManager = $metatagTagPluginManager; + } + + /** + * @inheritDoc + */ + public function supportsGeneration($object) { + if (!($object instanceof FieldDefinitionInterface)) { + return FALSE; + } + + return $object->getClass() == '\Drupal\metatag\Plugin\Field\MetatagEntityFieldItemList'; + } + + protected function getName($object) { + return 'MetatagField'; + } + + protected function getProperties($object, Settings $settings, Result $result, ComponentResult $component_result) { + return [ + 'value' => $component_result->getContext('tags'), + ]; + } + + protected function getMapping($object, $properties, Settings $settings, Result $result, ComponentResult $component_result) { + return 'value'; + } + + protected function generateInternal($object, Settings $settings, Result $result, ComponentResult $componentResult) { + $properties = $this->getProperties($object, $settings, $result, $componentResult); + $mapping = $this->getMapping($object, $properties, $settings, $result, $componentResult); + $name = $this->getName($object); + + $internal_component_result = $this->generatePropertiesComponentResult( + $properties, + $name, + 'Parsed' . $name, + Container::underscore($name) . '_parser', + $mapping, + $settings, + $result + ); + + $internal_component_result->setComponent( + 'parser', + $result->setComponent( + 'parser/' . Container::underscore($name) . '_parser', + 'const ' . Container::underscore($name) . '_parser' . ' = ' . + '(t: ' . $internal_component_result->getComponent('type') . '): ' . $internal_component_result->getComponent('target_type') . " => " . + "t ? (" . $this->generatePropertiesParserContent($properties, $mapping) . ") : {}" + ) + ); + + return $internal_component_result; + } + + public function generateTargetType($object, Settings $settings, Result $result, ComponentResult $componentResult) { + /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $object */ + return $componentResult->getContext('internal')->getComponent('target_type'); + } + + public function generateParser($object, Settings $settings, Result $result, ComponentResult $componentResult) { + /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $object */ + return $componentResult->getContext('internal')->getComponent('parser'); + } + + public function generateType($object, Settings $settings, Result $result, ComponentResult $componentResult) { + /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $object */ + return $componentResult->getContext('internal')->getComponent('type'); + } + + protected function preGenerate($object, Settings $settings, Result $result, ComponentResult $componentResult) { + parent::preGenerate($object, $settings, $result, $componentResult); + + $tags = $componentResult->getContext('tags'); + if (!isset($tags)) { + $tags = $this->generateTags($this->metatagTagPluginManager, $settings, $result, $componentResult); + $componentResult->setContext('tags', $tags); + } + + $internal = $componentResult->getContext('internal'); + if (!isset($internal)) { + $internal = $this->generateInternal($object, $settings, $result, $componentResult); + $componentResult->setContext('internal', $internal); + } + } + +} \ No newline at end of file diff --git a/ts_generator_metatag/src/ComponentGenerator/MetatagFieldGenerator.php b/ts_generator_metatag/src/ComponentGenerator/MetatagFieldGenerator.php new file mode 100644 index 0000000..214b328 --- /dev/null +++ b/ts_generator_metatag/src/ComponentGenerator/MetatagFieldGenerator.php @@ -0,0 +1,43 @@ +metatagTagPluginManager = $metatagTagPluginManager; + } + + protected function getItemProperties($object, Settings $settings, Result $result, ComponentResult $componentResult) { + return [ + 'value' => $componentResult->getContext('tags'), + ]; + } + + protected function preGenerate($object, Settings $settings, Result $result, ComponentResult $componentResult) { + $tags = $componentResult->getContext('tags'); + if (!isset($tags)) { + $tags = $this->generateTags($this->metatagTagPluginManager, $settings, $result, $componentResult); + $componentResult->setContext('tags', $tags); + } + + parent::preGenerate($object, $settings, $result, $componentResult); + } +} diff --git a/ts_generator_metatag/src/ComponentGenerator/TagGenerator.php b/ts_generator_metatag/src/ComponentGenerator/TagGenerator.php new file mode 100644 index 0000000..fe42a45 --- /dev/null +++ b/ts_generator_metatag/src/ComponentGenerator/TagGenerator.php @@ -0,0 +1,28 @@ +getDefinitions(); + + $properties = []; + foreach ($tags as $tag) { + $key = $tag['name']; + + $properties[$key . '?'] = 'string'; + } + + return $result->setComponent( + 'types/Metatags', + 'type Metatags = ' . $this->formatObject([], $properties) + ); + } +} \ No newline at end of file diff --git a/ts_generator_metatag/ts_generator_metatag.info.yml b/ts_generator_metatag/ts_generator_metatag.info.yml new file mode 100644 index 0000000..0620286 --- /dev/null +++ b/ts_generator_metatag/ts_generator_metatag.info.yml @@ -0,0 +1,8 @@ +name: TS Generator Metatag +type: module +description: Generate TypeScript code for metatag +package: Custom +core: 8.x +dependencies: + - ts_generator + - metatag diff --git a/ts_generator_metatag/ts_generator_metatag.services.yml b/ts_generator_metatag/ts_generator_metatag.services.yml new file mode 100644 index 0000000..1379928 --- /dev/null +++ b/ts_generator_metatag/ts_generator_metatag.services.yml @@ -0,0 +1,11 @@ +services: + ts_generator_metatag.component_generator.field.metatag_base: + class: Drupal\ts_generator_metatag\ComponentGenerator\MetatagBaseFieldGenerator + tags: + - { name: ts_generator_component, priority: 10 } + arguments: ['@plugin.manager.metatag.tag'] + ts_generator_metatag.component_generator.field.metatag: + class: Drupal\ts_generator_metatag\ComponentGenerator\MetatagFieldGenerator + tags: + - { name: ts_generator_component, priority: 10 } + arguments: ['@plugin.manager.field.field_type', '@plugin.manager.metatag.tag'] \ No newline at end of file