-
Notifications
You must be signed in to change notification settings - Fork 5
Simplify to register additional/custom Typir services #78
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
5f88820
simplified kind registry
JohannesMeierSE afea1cc
improved error message
JohannesMeierSE bb4a173
made test utility more flexible
JohannesMeierSE a6d81a2
fixed bug: the logic to ensure that types are not created multiple ti…
JohannesMeierSE 65d0faf
new feature: make the $names of kinds configurable
JohannesMeierSE 6ba8bef
enable additional services in Typir core with `createTypirServicesWit…
JohannesMeierSE 4ce6969
enable additional services in Typir-Langium with `createTypirLangiumS…
JohannesMeierSE 65aefc5
Langium services are stored in the TypirLangium services now
JohannesMeierSE d5d1421
rebasing fixes
JohannesMeierSE 2c7b673
demonstrate API for additional Typir services for the custom matrix t…
JohannesMeierSE faee192
polished the CHANGELOG.md
JohannesMeierSE d810785
specify sub-super-relationships of language nodes for the predefined …
JohannesMeierSE e027d6f
wrote documentation for customization
JohannesMeierSE ab78f9d
sketched alternative function to support additional services
JohannesMeierSE 01d1557
improvements according to the review
JohannesMeierSE 0c00785
removed skipped alternative solution, added forth customization param…
JohannesMeierSE File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,200 @@ | ||
| /****************************************************************************** | ||
| * Copyright 2025 TypeFox GmbH | ||
| * This program and the accompanying materials are made available under the | ||
| * terms of the MIT License, which is available in the project root. | ||
| ******************************************************************************/ | ||
|
|
||
| import { describe, expect, test } from 'vitest'; | ||
| import { ClassFactoryService, ClassKind } from '../src/kinds/class/class-kind.js'; | ||
| import { TestLanguageNode } from '../src/test/predefined-language-nodes.js'; | ||
| import { createTypirServices, createTypirServicesWithAdditionalServices, TypirServices } from '../src/typir.js'; | ||
| import { expectToBeType } from '../src/index-test.js'; | ||
| import { DefaultTypeConflictPrinter, isClassType, Type } from '../src/index.js'; | ||
|
|
||
| describe('Some examples how to customize the Typir services, focusing on adding another type factory', () => { | ||
|
|
||
| test('Demonstrate the default behaviour of classes', async () => { | ||
| // Use the default configuration of Typir | ||
| const typir = createTypirServices<TestLanguageNode>(); | ||
| // Create some classes | ||
| const classA = typir.factory.Classes.create({ className: 'A', fields: [], methods: [], superClasses: [] }).finish().getTypeFinal()!; | ||
| const classB = typir.factory.Classes.create({ className: 'B', fields: [], methods: [], superClasses: [] }).finish().getTypeFinal()!; | ||
| expectToBeType(classA, isClassType, type => type.className === 'A'); | ||
| expectToBeType(classB, isClassType, type => type.className === 'B'); | ||
| // Not more than 1 super-class are allowed: | ||
| expect(() => typir.factory.Classes.create({ className: 'C', fields: [], methods: [], superClasses: [classA, classB] }).finish()) | ||
| .toThrowError('Only 1 super-class is allowed.'); | ||
| }); | ||
|
|
||
| test('Update an existing type factory', async () => { | ||
| // The service for creating classes already exists in the Typir services, but its implementation is configured: | ||
| // - Here, only an option of the existing implementation is changed. | ||
| // - But in general you could add a completely new implementation here. | ||
| const typir = createTypirServices<TestLanguageNode>({ | ||
| factory: { | ||
| Classes: services => new ClassKind(services, { maximumNumberOfSuperClasses: 2 }), | ||
| }, | ||
| }); | ||
| // Create some classes | ||
| const classA = typir.factory.Classes.create({ className: 'A', fields: [], methods: [], superClasses: [] }).finish().getTypeFinal()!; | ||
| const classB = typir.factory.Classes.create({ className: 'B', fields: [], methods: [], superClasses: [] }).finish().getTypeFinal()!; | ||
| expectToBeType(classA, isClassType, type => type.className === 'A'); | ||
| expectToBeType(classB, isClassType, type => type.className === 'B'); | ||
| // 2 super-classes are fine now: | ||
| const classC = typir.factory.Classes.create({ className: 'C', fields: [], methods: [], superClasses: [classA, classB] }).finish().getTypeFinal()!; | ||
| expect(classC).toBeTruthy(); | ||
| }); | ||
|
|
||
| test('Add another type factory', async () => { | ||
| // Make the additional service explicit: | ||
| // In general, you can add an arbitrary number of services, which might be deeply nested | ||
| type AdditionalExampleTypirServices = { | ||
| readonly factory: { | ||
| readonly OtherClasses: ClassFactoryService<TestLanguageNode>; | ||
| }, | ||
| }; | ||
| type ExampleTypirServices = TypirServices<TestLanguageNode> & AdditionalExampleTypirServices; | ||
|
|
||
| // Instantiate the services and provide implementations for all added services. | ||
| const typir: ExampleTypirServices = createTypirServicesWithAdditionalServices<TestLanguageNode, AdditionalExampleTypirServices>({ | ||
| factory: { | ||
| // Here we reuse the existing class kind implementation, but with a different configuration to demonstrate types with a different behaviour: | ||
| OtherClasses: services => new ClassKind(services, { maximumNumberOfSuperClasses: 2, $name: 'OtherClass' }), | ||
| }, | ||
| }); | ||
|
|
||
| // Default classes: not more than 1 super-class | ||
| const classA = typir.factory.Classes.create({ className: 'A', fields: [], methods: [], superClasses: [] }).finish().getTypeFinal()!; | ||
| const classB = typir.factory.Classes.create({ className: 'B', fields: [], methods: [], superClasses: [] }).finish().getTypeFinal()!; | ||
| expectToBeType(classA, isClassType, type => type.className === 'A'); | ||
| expectToBeType(classB, isClassType, type => type.className === 'B'); | ||
| expect(() => typir.factory.Classes.create({ className: 'C', fields: [], methods: [], superClasses: [classA, classB] }).finish()) | ||
| .toThrowError('Only 1 super-class is allowed.'); | ||
|
|
||
| // New classes: 2 super-classes are fine now | ||
| const classD = typir.factory.OtherClasses.create({ className: 'D', fields: [], methods: [], superClasses: [] }).finish().getTypeFinal()!; | ||
| const classE = typir.factory.OtherClasses.create({ className: 'E', fields: [], methods: [], superClasses: [] }).finish().getTypeFinal()!; | ||
| const classF = typir.factory.OtherClasses.create({ className: 'F', fields: [], methods: [], superClasses: [classD, classE] }).finish().getTypeFinal()!; | ||
| expect(classF).toBeTruthy(); | ||
| expectToBeType(classD, isClassType, type => type.className === 'D'); | ||
| expectToBeType(classE, isClassType, type => type.className === 'E'); | ||
| expectToBeType(classF, isClassType, type => type.className === 'F'); | ||
|
JohannesMeierSE marked this conversation as resolved.
|
||
| }); | ||
|
|
||
| test('Newly added services are usable by all other services', async () => { | ||
| // new service | ||
| interface TestService { | ||
| doSomething(): string; | ||
| } | ||
| type AdditionalExampleTypirServices = { | ||
| TestService: TestService; | ||
| }; | ||
| // Defining this TypeScript type is not mandatory, but makes the customization with additional services easier (search for the use of this type!) | ||
|
insafuhrmann marked this conversation as resolved.
Outdated
|
||
| type ExampleTypirServices = TypirServices<TestLanguageNode> & AdditionalExampleTypirServices; | ||
|
|
||
| // implementation for the new service | ||
| class TestServiceImpl implements TestService { | ||
| readonly services: ExampleTypirServices; | ||
| constructor(services: ExampleTypirServices) { | ||
| this.services = services; | ||
| } | ||
| doSomething(): string { | ||
| // all services are usable here! | ||
| this.services.Assignability; // existing service | ||
| this.services.TestService; // new service | ||
| return 'something'; | ||
| } | ||
| } | ||
|
|
||
| // adapted implementation for an existing service | ||
| class ExamplePrinter extends DefaultTypeConflictPrinter<TestLanguageNode> { | ||
| readonly services: ExampleTypirServices; | ||
| constructor(services: ExampleTypirServices) { | ||
| super(); | ||
| this.services = services; | ||
| } | ||
| override printTypeName(type: Type): string { | ||
| // new services are usable in (adapted) implementations for existing services | ||
| return `${this.services.TestService.doSomething()}--${super.printTypeName(type)}`; | ||
| } | ||
| } | ||
|
|
||
| // Instantiate the Typir services and provide implementations for all added and customized services: | ||
| const typir: ExampleTypirServices = createTypirServicesWithAdditionalServices<TestLanguageNode, AdditionalExampleTypirServices>( | ||
| // 1st argument: Specify implementations for all new services | ||
| { | ||
| TestService: services => new TestServiceImpl(services), | ||
| }, | ||
| // 2nd argument: Customize some existing services here | ||
| // In general, the following optional arguments might customize all services (default and added ones) | ||
| { | ||
| Printer: services => new ExamplePrinter(services), | ||
| }, | ||
| // some more optional customizations might be added here for convenience | ||
| ); | ||
|
|
||
| // Create a type and check the new prefix | ||
| const type = typir.factory.Primitives.create({ primitiveName: 'ABC' }).finish(); | ||
| expect(typir.Printer.printTypeName(type)).toBe('something--ABC'); | ||
| }); | ||
|
|
||
| test('Ensure unique names/identifiers when using different instances of the same kind class in parallel', async () => { | ||
| // This test case demonstrates some issues and how to solve them for the Classes case. | ||
| // Depending on the kind, not all of theses issues occur or occur in a different way. | ||
| // This test case aims to point to these issues in general. | ||
| type AdditionalExampleTypirServices = { | ||
| readonly factory: { | ||
| readonly OtherClasses: ClassFactoryService<TestLanguageNode>; | ||
| }, | ||
| }; | ||
| type ExampleTypirServices = TypirServices<TestLanguageNode> & AdditionalExampleTypirServices; | ||
|
|
||
| // Reusing the following default implementation causes some issues with unique names ... | ||
| let typir: ExampleTypirServices = createTypirServicesWithAdditionalServices<TestLanguageNode, AdditionalExampleTypirServices>({ | ||
| factory: { | ||
| OtherClasses: services => new ClassKind(services), | ||
| }, | ||
| }); | ||
|
|
||
| // Each kind needs to have a unique $name | ||
| expect(typir.factory.Classes).toBeTypeOf('object'); // trigger to create the default class factory, since they are created lazily | ||
| expect(() => typir.factory.OtherClasses).toThrowError("duplicate kind named 'ClassKind'"); | ||
| typir = createTypirServicesWithAdditionalServices<TestLanguageNode, AdditionalExampleTypirServices>({ | ||
| factory: { | ||
| OtherClasses: services => new ClassKind(services, { | ||
| $name: 'OtherClass', // specify another $name for the new kind | ||
| }), | ||
| }, | ||
| }); | ||
| expect(typir.factory.Classes).toBeTypeOf('object'); | ||
| expect(typir.factory.OtherClasses).toBeTypeOf('object'); // now both kinds are available and have different $names | ||
|
|
||
| // Types need to have unique identifiers: this is ensured by having unique prefixes | ||
| expectToBeType(typir.factory.Classes.create({ className: 'A', fields: [], methods: [] }).finish().getTypeFinal(), isClassType, type => type.className === 'A'); | ||
| expect(() => typir.factory.OtherClasses.create({ className: 'A', fields: [], methods: [] }).finish()) | ||
| .toThrowError("A new type with identifier 'class-A' and kind 'OtherClass' (implemented in ClassKind) shall be created, but there is already a type with identifier 'class-A' and kind 'ClassKind' (implemented in ClassKind) in the type graph."); | ||
|
insafuhrmann marked this conversation as resolved.
Outdated
|
||
| typir = createTypirServicesWithAdditionalServices<TestLanguageNode, AdditionalExampleTypirServices>({ | ||
| factory: { | ||
| OtherClasses: services => new ClassKind(services, { | ||
| $name: 'OtherClass', | ||
| identifierPrefix: 'other-class', // unique prefix for types of this kind | ||
| }), | ||
| }, | ||
| }); | ||
| expectToBeType(typir.factory.Classes.create({ className: 'A', fields: [], methods: [] }).finish().getTypeFinal(), isClassType, type => type.className === 'A'); | ||
| expectToBeType(typir.factory.OtherClasses.create({ className: 'A', fields: [], methods: [] }).finish().getTypeFinal(), isClassType, type => type.className === 'A'); | ||
| }); | ||
|
|
||
| test('Removing an existing type factory', async () => { | ||
| // Removing an existing type factory is not possible and does not make sense, since other default services might use this service. | ||
| // - Simple approach: Just don't use this service anymore. | ||
| // - More explicit approach: Throw an exception whenever this service is used, as demonstrated here: | ||
| const typir = createTypirServices<TestLanguageNode>({ | ||
| factory: { | ||
| Classes: () => { throw new Error('Do not use classes!'); }, | ||
| }, | ||
| }); | ||
| expect(() => typir.factory.Classes).toThrowError('Do not use classes!'); | ||
| }); | ||
|
|
||
| }); | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was three before, but right now I am a bit bewildered. Why three? Is it just an exemplary choice?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, in the end, three is an arbitrary number here. I am fine to add even more optional customizations. For me, it sounds like a good idea to some more in order to make it even more flexible.
injectindependency-injection.tsfrom Langium has even 8 optional customizations ...