diff --git a/.github/workflows/sdk-tests.yml b/.github/workflows/sdk-tests.yml index 87b46829a..66b4d0146 100644 --- a/.github/workflows/sdk-tests.yml +++ b/.github/workflows/sdk-tests.yml @@ -150,3 +150,31 @@ jobs: git diff examples/python/generated exit 1 fi + + test-mustache-java-r4-example: + runs-on: ubuntu-latest + + strategy: + matrix: + bun-version: [latest] + java-version: [21, 25] + + steps: + - uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: ${{ matrix.bun-version }} + + - name: Setup Java + uses: actions/setup-java@v4 + with: + java-version: ${{ matrix.java-version }} + distribution: "temurin" + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Run tests + run: make test-mustache-java-r4-example diff --git a/.gitignore b/.gitignore index 01cd89285..06b0970e2 100644 --- a/.gitignore +++ b/.gitignore @@ -49,5 +49,8 @@ examples/csharp/obj examples/typescript-ccda/fhir-types examples/typescript-ccda/tree.yaml examples/typescript-ccda/type-schemas + examples/typescript-sql-on-fhir/fhir-types examples/typescript-sql-on-fhir/tree.yaml + +examples/mustache/mustache-java-r4-output diff --git a/Makefile b/Makefile index be348a29f..9872fe396 100644 --- a/Makefile +++ b/Makefile @@ -61,6 +61,10 @@ test-typescript-ccda-example: typecheck format lint bun run examples/typescript-ccda/generate.ts $(TYPECHECK) --project examples/typescript-ccda/tsconfig.json +test-mustache-java-r4-example: typecheck format lint + bun run examples/mustache/mustache-java-r4-gen.ts + $(TYPECHECK) --project examples/mustache/tsconfig.examples-mustache.json + test-csharp-sdk: typecheck format prepare-aidbox-runme lint $(TYPECHECK) --project examples/csharp/tsconfig.json bun run examples/csharp/generate.ts diff --git a/README.md b/README.md index 598994a68..dd5f498a2 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ - [Field-Level Tree Shaking](#field-level-tree-shaking) - [Generation](#generation) - [1. Writer-Based Generation (Programmatic)](#1-writer-based-generation-programmatic) + - [2. Mustache Template-Based Generation (Declarative)](#2-mustache-template-based-generation-declarative) - [Roadmap](#roadmap) - [Support](#support) - [Footnotes](#footnotes) @@ -33,6 +34,7 @@ A powerful, extensible code generation toolkit for FHIR ([Fast Healthcare Intero Guides: - **[Writer Generator Guide](docs/guides/writer-generator.md)** - Build custom code generators with the Writer base class +- **[Mustache Generator Guide](docs/guides/mustache-generator.md)** - Template-based code generation for any language - **[TypeSchemaIndex Guide](docs/guides/typeschema-index.md)** - Type Schema structure and utilities - **[Testing Generators Guide](docs/guides/testing-generators.md)** - Unit tests, snapshot testing, and best practices - **[Contributing Guide](CONTRIBUTING.md)** - Development setup and workflow @@ -98,6 +100,7 @@ See the [examples/](examples/) directory for working demonstrations: - **[typescript-sql-on-fhir/](examples/typescript-sql-on-fhir/)** - SQL on FHIR ViewDefinition with tree shaking - **[python/](examples/python/)** - Python/Pydantic model generation with configurable field formats - **[csharp/](examples/csharp/)** - C# class generation with namespace configuration +- **[mustache/](examples/mustache/)** - Java generation with Mustache templates and post-generation hooks - **[local-package-folder/](examples/local-package-folder/)** - Loading unpublished local FHIR packages For detailed documentation, see [examples/README.md](examples/README.md). @@ -247,6 +250,20 @@ Each language writer maintains full control over output formatting while leverag **When to use**: Full control needed, complex generation logic, performance-critical, language has a dedicated writer, production-grade output +#### 2. Mustache Template-Based Generation (Declarative) + +For custom languages or formats, use Mustache templates to define code generation rules without programming: + +- **Template Files**: Declarative Mustache templates that describe output structure +- **Configuration**: JSON config file controlling type filtering, naming, and post-generation hooks +- **ViewModels**: Type Schema automatically transformed into template-friendly data structures + +Templates enable flexible code generation for any language or format (Go, Rust, GraphQL, documentation, configs) by describing the output format rather than implementing generation logic. + +**When to use**: Custom language support, quick prototyping, template-driven customization, non-code output + +--- + ## Roadmap - [x] TypeScript generation diff --git a/bun.lock b/bun.lock index 9b04e8493..8a4e885a4 100644 --- a/bun.lock +++ b/bun.lock @@ -6,6 +6,7 @@ "dependencies": { "@atomic-ehr/fhir-canonical-manager": "canary", "@atomic-ehr/fhirschema": "^0.0.5", + "mustache": "^4.2.0", "picocolors": "^1.1.1", "tinyglobby": "^0.2.15", "yaml": "^2.8.2", @@ -14,6 +15,7 @@ "devDependencies": { "@biomejs/biome": "^2.3.9", "@types/bun": "^1.3.4", + "@types/mustache": "^4.2.6", "@types/node": "^22.19.3", "@types/yargs": "^17.0.35", "knip": "^5.73.4", @@ -213,6 +215,8 @@ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/mustache": ["@types/mustache@4.2.6", "", {}, "sha512-t+8/QWTAhOFlrF1IVZqKnMRJi84EgkIK5Kh0p2JV4OLywUvCwJPFxbJAl7XAow7DVIHsF+xW9f1MVzg0L6Szjw=="], + "@types/node": ["@types/node@22.19.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA=="], "@types/yargs": ["@types/yargs@17.0.35", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg=="], @@ -337,6 +341,8 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="], + "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], diff --git a/docs/guides/mustache-generator.md b/docs/guides/mustache-generator.md new file mode 100644 index 000000000..20ad3fa7b --- /dev/null +++ b/docs/guides/mustache-generator.md @@ -0,0 +1,623 @@ +# Creating Custom Code Generators with Mustache Templates + +This guide explains how to build a custom code generator using Mustache templates in Atomic EHR Codegen. Unlike the `Writer` class which requires implementing TypeScript code, the Mustache generator uses template files, making it ideal for generating any language or format without programming. + + +**Table of Contents** + +- [Creating Custom Code Generators with Mustache Templates](#creating-custom-code-generators-with-mustache-templates) + - [Architecture Overview](#architecture-overview) + - [Template Project Structure](#template-project-structure) + - [Configuration File](#configuration-file) + - [Rendering](#rendering) + - [Name Transformations](#name-transformations) + - [Unsafe Character Pattern](#unsafe-character-pattern) + - [Hook Execution Control](#hook-execution-control) + - [Post-Generation Hooks](#post-generation-hooks) + - [Template Files](#template-files) + - [Template Input](#template-input) + - [TypeViewModel](#typeviewmodel) + - [FieldViewModel](#fieldviewmodel) + - [EnumViewModel](#enumviewmodel) + - [Writing Mustache Templates](#writing-mustache-templates) + - [Template Syntax](#template-syntax) + - [Case Conversion Lambdas](#case-conversion-lambdas) + - [Name Safety Lambdas](#name-safety-lambdas) + - [Special Variables](#special-variables) + - [Static Files](#static-files) + - [Debugging & Testing](#debugging--testing) + - [Enable Debug Output](#enable-debug-output) + - [Inspect Generated Models](#inspect-generated-models) + - [Validate Templates](#validate-templates) + - [Resources](#resources) + + + +--- + +## Architecture Overview + +The Mustache code generation pipeline is part of the three-stage system: + +```text +Register extends CanonicalManager (FHIR Package retrieval and FHIR Schema generation) + ↓ +TypeSchemaIndex (Type Schema generation and management) + ↓ +MustacheGenerator (Template-based generation) ← [User template project] + ↓ +Generated Code +``` + +For comprehensive documentation on `TypeSchemaIndex` structure, utilities, and usage, see the [TypeSchemaIndex Guide](./typeschema-index.md). + +The template-based generation consists of two main components: + +1. `ViewModelFactory` + + Transforms TypeSchema data into template-friendly structures. + + - Converts FHIR types to ViewModels + - Applies case conversion and name transformations + - Resolves type dependencies + - Handles nested types and enums + +2. `MustacheGenerator` + + Renders templates with ViewModel data. + + - Loads and caches template files + - Processes configuration (filters, rendering rules) + - Manages file output with directory hierarchy + - Executes post-generation hooks + +--- + +## Template Project Structure + +A Mustache template project follows this layout: + +``` +my-template/ +├── config.json # Generator configuration +├── templates/ # Mustache template files +│ ├── resource.mustache # Template for resources +│ ├── complex-type.mustache # Template for complex types +│ ├── utility.mustache # Template for utility/shared files +│ └── partials/ # Reusable template fragments +│ ├── header.mustache +│ ├── imports.mustache +│ └── field-definition.mustache +├── static/ # Static files copied as-is +│ ├── package.json +│ ├── tsconfig.json +│ ├── README.md +│ └── .gitignore +└── README.md # Documentation for your template +``` + +### Configuration File + +The `config.json` file defines how types are rendered and processed: + +```jsonc +{ + // Enable debug output in templates: "OFF" | "FORMATTED" | "COMPACT" + "debug": "OFF", + + // Metadata injected into all templates + "meta": { + "generator": "My FHIR Code Generator v1.0" + }, + + // Define how each type category is generated (resource, complexType, utility) + "renderings": { + "resource": [ + { + "source": "resource.mustache", + "path": "models", + "fileNameFormat": "%s.ts", + "filter": { + "whitelist": ["Patient", "Observation"], + "blacklist": [] + } + } + ], + "complexType": [ + { + "source": "type.mustache", + "path": "types", + "fileNameFormat": "%s.ts" + } + ], + "utility": [ + { + "source": "index.mustache", + "path": ".", + "fileNameFormat": "index.ts" + } + ] + }, + + // Reserved words to escape in names + "keywords": ["class", "interface", "type", "const", "let", "var"], + + // Map FHIR types to target language types + "primitiveTypeMap": { + "string": "string", + "boolean": "boolean", + "integer": "number", + "decimal": "number", + "date": "Date", + "dateTime": "Date", + "time": "string" + }, + + // Name transformation rules for types, fields, and enums + "nameTransformations": { + "type": [], + "field": [], + "enum": [] + }, + + // Pattern for unsafe characters to replace in names + "unsaveCharacterPattern": "[^a-zA-Z0-9_]", + + // Whether to execute post-generation hooks + "shouldRunHooks": true, + + // Commands to run after generation + "hooks": { + "afterGenerate": [ + { + "cmd": "prettier", + "args": ["--write", "."] + } + ] + } +} +``` + +#### Rendering + +Each rendering defines output for one template: + +```typescript +type Rendering = { + source: string; // Template file (relative to templates/) + path: string; // Output directory (relative to outputDir) + fileNameFormat: string; // Output filename with %s for model.saveName + filter?: FilterType; // Optional whitelist/blacklist + properties?: Record; // Custom properties for this rendering +}; +``` + +The `fileNameFormat` uses `%s` as a placeholder for the model's safe name: +- `"%s.ts"` with model "Patient" → `Patient.ts` +- `"%s.model.ts"` → `Patient.model.ts` + + +`Filter` allows performing rendering-level filters. + +Apply different filters per rendering: + +```json +{ + "renderings": { + "resource": [ + { + "source": "resource.mustache", + "fileNameFormat": "%s.ts", + "filter": { + "whitelist": ["Patient"] + } + }, + { + "source": "resource.builder.mustache", + "fileNameFormat": "%sBuilder.ts", + "filter": { + "whitelist": ["Patient", "Observation"] + } + } + ] + } +} +``` + +**Filter Logic:** +- Empty whitelist and blacklist: process all types +- Whitelist specified: only process matching types +- Blacklist specified: process all except matching types +- Both specified: whitelist checked first, then blacklist + +**Pattern Examples:** + +```json +{ + "whitelist": [ + "Patient", {{! Exact match }} + "^Observation.*", {{! Starts with }} + ".*Bundle$", {{! Ends with }} + ".*Element.*" {{! Contains }} + ], + "blacklist": [ + "_.*", {{! Internal types }} + ".*Meta" {{! Meta types }} + ] +} +``` + + +#### Name Transformations + +The `nameTransformations` option applies transformation rules to identifiers: + +```json +{ + "nameTransformations": { + "type": [ + { "pattern": "^CodeableConcept$", "replacement": "Coding" }, + { "pattern": "^Period$", "replacement": "DateRange" } + ], + "field": [ + { "pattern": "^resourceType$", "replacement": "type" } + ], + "enum": [ + { "pattern": "^(active|inactive)$", "replacement": "Status" } + ] + } +} +``` + +Each category (`type`, `field`, `enum`) accepts an array of transformation rules. + +#### Unsafe Character Pattern + +The `unsaveCharacterPattern` specifies which characters should be escaped or removed from identifiers: + +```json +{ + "unsaveCharacterPattern": "[^a-zA-Z0-9_]" +} +``` + +This pattern matches invalid characters that may appear in FHIR names but aren't valid in your target language. Matching characters are replaced with underscores. + +#### Hook Execution Control + +The `shouldRunHooks` flag determines whether post-generation hooks are executed: + +```json +{ + "shouldRunHooks": true +} +``` + +Set to `false` to skip hook execution (useful for debugging or dry runs). + +#### Post-Generation Hooks + +Hooks execute commands after all files are generated. Use them for formatting, linting, or building. + +```json +{ + "hooks": { + "afterGenerate": [ + { + "cmd": "prettier", + "args": ["--write", "."] + }, + { + "cmd": "eslint", + "args": ["--fix", "."] + } + ] + } +} +``` + +**Hook Execution:** +- Runs sequentially in order +- Working directory is the output directory +- Output is shown to the user +- Aborts generation if any hook fails + +### Template Files + +Each `.mustache` file receives a ViewModel and renders output: + +```mustache +{{! templates/resource.mustache }} +{{> partials/header }} + +export interface {{#lambda.pascalCase}}{{model.name}}{{/lambda.pascalCase}} { + {{#model.fields}} + {{#lambda.camelCase}}{{name}}{{/lambda.camelCase}}{{^isRequired}}?{{/isRequired}}: {{typeName}}; + {{/model.fields}} +} +``` + +Partials are reusable template fragments included via `{{> partials/name }}`: + +```mustache +{{! templates/partials/header.mustache }} +/** + * Generated by {{meta.generator}} + * {{meta.timestamp}} + * DO NOT EDIT - This file is autogenerated + */ +``` + +Use partials to: +- Share common headers and imports +- Reduce template duplication +- Organize complex generation logic + +#### Template Input + +ViewModels transform TypeSchema into template-friendly data structures. The `ViewModelFactory` creates them automatically. + +##### TypeViewModel + +Represents a FHIR resource or complex type: + +```typescript +{ + name: string; // "Patient" + saveName: string; // Escaped if keyword + schema: TypeSchema; // Original FHIR definition + + fields: FieldViewModel[]; // Properties/fields + dependencies: { + resources: NamedViewModel[]; // Referenced resources + complexTypes: NamedViewModel[]; // Referenced types + }; + + nestedComplexTypes: TypeViewModel[]; // Nested types + nestedEnums: EnumViewModel[]; // Value set enums + + hasFields: boolean; + hasNestedComplexTypes: boolean; + hasNestedEnums: boolean; + isNested: boolean; + isComplexType: Record; + isResource: Record; +} +``` + +Access in templates: + +```mustache +{{model.name}} {{! Type name }} +{{model.saveName}} {{! Safe identifier }} +{{#model.fields}} {{! Iterate fields }} + {{name}} + {{typeName}} + {{isRequired}} +{{/model.fields}} +``` + +##### FieldViewModel + +Represents a field within a type: + +```typescript +{ + name: string; // "identifier" + saveName: string; // Escaped name + owner: NamedViewModel; // Parent type + + typeName: string; // "Identifier | CodeableConcept" + + isArray: boolean; // true if cardinality > 1 + isRequired: boolean; // true if min cardinality > 0 + isEnum: boolean; // true if bound to value set + + isSizeConstrained: boolean; + min?: number; // Minimum length/count + max?: number; // Maximum length/count + + isPrimitive: { + isString?: boolean; + isBoolean?: boolean; + isInteger?: boolean; + isDate?: boolean; + }; + isComplexType: { isIdentifier?: boolean; ... }; + isResource: { isPatient?: boolean; ... }; +} +``` + +Access in templates: + +```mustache +{{#model.fields}} + {{#isRequired}}required{{/isRequired}} + {{#isArray}}list{{/isArray}} + {{#isPrimitive.isString}}string type{{/isPrimitive.isString}} +{{/model.fields}} +``` + +##### EnumViewModel + +Represents a value set binding: + +```typescript +{ + name: string; // "PatientStatus" + saveName: string; // Safe identifier + + values: [ + { name: "active", saveName: "Active" }, + { name: "inactive", saveName: "Inactive" }, + { name: "entered-in-error", saveName: "EnteredInError" } + ]; +} +``` + +Access in templates: + +```mustache +{{#model.nestedEnums}} +export enum {{#lambda.pascalCase}}{{name}}{{/lambda.pascalCase}} { + {{#values}} + {{saveName}} = "{{name}}", + {{/values}} +} +{{/model.nestedEnums}} +``` + +### Writing Mustache Templates + +#### Template Syntax + +Mustache uses `{{variable}}` syntax for data binding: + +```mustache +{{variable}} {{! Output variable }} +{{#section}}...{{/section}} {{! Section (if true or iterate array) }} +{{^section}}...{{/section}} {{! Inverted (if false) }} +{{#lambda}}text{{/lambda}} {{! Lambda (transformation) }} +{{> partials/name }} {{! Include partial }} +{{! comment }} {{! Comment (not output) }} +{{{variable}}} {{! Unescaped output }} +``` + +#### Case Conversion Lambdas + +Transform text case within templates: + +```mustache +{{#lambda.camelCase}}Patient Name{{/lambda.camelCase}} +{{! Output: patientName }} + +{{#lambda.pascalCase}}patient name{{/lambda.pascalCase}} +{{! Output: PatientName }} + +{{#lambda.snakeCase}}Patient Name{{/lambda.snakeCase}} +{{! Output: patient_name }} + +{{#lambda.kebabCase}}Patient Name{{/lambda.kebabCase}} +{{! Output: patient-name }} + +{{#lambda.lowerCase}}PATIENT NAME{{/lambda.lowerCase}} +{{! Output: patient name }} + +{{#lambda.upperCase}}patient name{{/lambda.upperCase}} +{{! Output: PATIENT NAME }} +``` + +#### Name Safety Lambdas + +Apply name transformations and keyword escaping: + +```mustache +{{#lambda.saveTypeName}}{{model.name}}{{/lambda.saveTypeName}} +{{! Applies keyword escaping and type name rules }} + +{{#lambda.saveFieldName}}{{field.name}}{{/lambda.saveFieldName}} +{{! Escapes reserved words for field names }} + +{{#lambda.saveEnumValueName}}{{value.name}}{{/lambda.saveEnumValueName}} +{{! Creates safe enum value identifiers }} +``` + +#### Special Variables + +Available in every template: + +```mustache +{{meta.timestamp}} {{! ISO 8601 timestamp }} +{{meta.generator}} {{! Generator name from config }} +{{properties.*}} {{! Custom properties from rendering config }} +``` + +**List Context Variables** (when iterating arrays): + +```mustache +{{#model.fields}} + {{-index}} {{! 0-based position }} + {{-length}} {{! Total count }} + {{-first}} {{! true if first item }} + {{-last}} {{! true if last item }} +{{/model.fields}} +``` + +### Static Files + +Files in the `static/` directory are copied to output unchanged: +- Build configuration (tsconfig.json, go.mod, Cargo.toml, etc.) +- Package manifests (package.json, setup.py, etc.) +- Documentation (README.md) +- Ignore files (.gitignore, .dockerignore, etc.) + +--- + +## Debugging & Testing + +### Enable Debug Output + +Set `debug` in config.json: + +```json +{ + "debug": "FORMATTED" +} +``` + +This injects a `{{debug}}` variable in templates with the full model structure. Use in templates: + +```mustache +
+{{debug}}
+
+``` + +**Debug Modes:** +- `"OFF"`: No debug info (default) +- `"FORMATTED"`: Pretty-printed JSON +- `"COMPACT"`: Minified JSON + +### Inspect Generated Models + +Use in-memory generation to inspect ViewModels: + +```typescript +import { createGenerator } from "@atomic-ehr/codegen"; + +const generator = createGenerator("./my-template", { + outputDir: "/tmp/test", + inMemoryOnly: true +}); + +const tsIndex = await builder.build(); +await generator.generate(tsIndex); + +const files = generator.writtenFiles(); +console.log(files[0].content); +``` + +### Validate Templates + +Check Mustache syntax: + +```typescript +import Mustache from "mustache"; + +const template = fs.readFileSync("templates/resource.mustache", "utf-8"); +const model = { /* sample ViewModel */ }; + +try { + const output = Mustache.render(template, model); + console.log("Template is valid"); +} catch (e) { + console.error("Template error:", e.message); +} +``` + +--- + +## Resources + +- **TypeSchemaIndex Guide**: [typeschema-index.md](./typeschema-index.md) - Input data structure and utilities +- **Writer Generator Guide**: [writer-generator.md](./writer-generator.md) - Building custom code generators with TypeScript diff --git a/docs/guides/typeschema-index.md b/docs/guides/typeschema-index.md index 45b76cdd0..bceadcb61 100644 --- a/docs/guides/typeschema-index.md +++ b/docs/guides/typeschema-index.md @@ -29,7 +29,7 @@ The `TypeSchemaIndex` is a comprehensive data structure containing all transform ## Overview -TypeSchemaIndex is created during the TypeSchema generation phase and serves as the input to all code generators (Writer implementations, etc.). It organizes all FHIR schemas by canonical URL and package name, enabling efficient lookups and traversals of complex type relationships. +TypeSchemaIndex is created during the TypeSchema generation phase and serves as the input to all code generators (Writer implementations, Mustache templates, etc.). It organizes all FHIR schemas by canonical URL and package name, enabling efficient lookups and traversals of complex type relationships. Why TypeSchemaIndex Matters @@ -201,4 +201,5 @@ const sorted = sortAsDeclarationSequence(byPackage["hl7.fhir.r4.core"]); - **TypeSchemaIndex & Helper Utilities Implementation**: `src/typeschema/utils.ts` - **TypeSchema Type Definition**: `src/typeschema/types.ts` - **Writer Generator Guide**: [writer-generator.md](./writer-generator.md) +- **Mustache Generator Guide**: [mustache-generator.md](./mustache-generator.md) - **FHIR Specification**: [https://www.hl7.org/fhir/](https://www.hl7.org/fhir/) diff --git a/examples/README.md b/examples/README.md index 890149b7c..1bbebcf0a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -6,67 +6,53 @@ This directory contains working examples demonstrating the capabilities of Atomi ### TypeScript Generation -- **[typescript-r4/](typescript-r4/)** - FHIR R4 core type generation ✅ Code included - - Full FHIR R4 resource type definitions with profiles and bundles - - Demonstrates `attach` and `extract` functions for FHIR profiles - - Includes demo showing resource creation and bundle composition - -- **[typescript-ccda/](typescript-ccda/)** - C-CDA on FHIR type generation ✅ Code included - - HL7 CDA UV Core package (`hl7.cda.uv.core@2.0.1-sd`) - - Document structure and clinical content models - -- **[typescript-sql-on-fhir/](typescript-sql-on-fhir/)** - SQL on FHIR ViewDefinition types ✅ Code included - - Remote TGZ package loading from build.fhir.org - - Demonstrates tree shaking to include only ViewDefinition - - Shows SQL-on-FHIR view definition patterns - -- **[local-package-folder/](local-package-folder/)** - Custom FHIR packages from local files ✅ Code included - - Load unpublished StructureDefinitions from disk - - Combine local and published packages - - Dependency resolution with FHIR R4 core - - Tree shaking for custom logical models - -### Python Generation - -- **[python/](python/)** - Python/Pydantic model generation ✅ Code included - - Full FHIR R4 as Pydantic models - - Configurable field formats (`snake_case` or `camelCase`) - - Automatic validation and serialization with Pydantic - - Virtual environment setup instructions - - Client implementation example: [examples/python/client.py](examples/python/client.py). +- **[typescript-r4/](typescript-r4/)** - FHIR R4 core type generation + - `generate.ts` - Generates TypeScript interfaces for FHIR R4 specification + - `demo.ts` - Demonstrates resource creation, profile usage (bodyweight), and bundle composition + - Shows how to use `attach` and `extract` functions for FHIR profiles -### C# Generation +- **[typescript-ccda/](typescript-ccda/)** - C-CDA on FHIR type generation + - `generate.ts` - Generates types from HL7 CDA UV Core package (`hl7.cda.uv.core@2.0.1-sd`) + - Exports TypeSchema files and dependency tree -- **[csharp/](csharp/)** - C# class generation ✅ Code included - - Full FHIR R4 as C# classes - - Custom namespace support - - Integration tests with Aidbox FHIR server +- **[typescript-sql-on-fhir/](typescript-sql-on-fhir/)** - SQL on FHIR ViewDefinition types + - `generate.ts` - Generates types from remote TGZ package + - Demonstrates tree shaking to include only specific resources -## Prerequisites +### Multi-Language Generation -- **Bun 1.0+** or Node.js 18+ -- **RAM**: 1GB+ minimum, 2GB+ recommended for full R4 package generation -- **Python 3.10+** (for Python example only) -- **.NET 6.0+** (for C# example only) -- **Docker & Docker Compose** (optional, for C# Aidbox integration tests) +- **[python/](python/)** - Python/Pydantic model generation + - `generate.ts` - Generates Python models with configurable field formats + - Supports `snake_case` or `camelCase` field naming + - Configurable extra field validation + - Client implementation example: [examples/python/client.py](examples/python/client.py). -## Quick Start -### Setup +- **[csharp/](csharp/)** - C# class generation + - `generate.ts` - Generates C# classes with custom namespace + - Includes static files for base functionality + - Includes integration tests with Aidbox FHIR server -From the project root: +### Template-Based Generation -```bash -cd codegen -bun install -``` +- **[mustache/](mustache/)** - Java generation with Mustache templates + - `mustache-java-r4-gen.ts` - Generates Java code using Mustache templates + - Full Maven project structure with post-generation hooks + - Demonstrates template-driven code generation for any language or format + +### Local Package Support -### Generate Types +- **[local-package-folder/](local-package-folder/)** - Working with unpublished FHIR packages + - `generate.ts` - Loads local StructureDefinitions from disk + - Demonstrates dependency resolution with FHIR R4 core + - Shows tree shaking for custom logical models -Run any example with: +## Running Examples + +Each example contains a `generate.ts` script that can be run with: ```bash -# Using Bun (recommended) +# Using Bun bun run examples/typescript-r4/generate.ts # Using Node with tsx @@ -76,72 +62,31 @@ npx tsx examples/typescript-r4/generate.ts npx ts-node examples/typescript-r4/generate.ts ``` -Replace `typescript-r4` with any example name: -- `typescript-ccda` -- `typescript-sql-on-fhir` -- `local-package-folder` -- `python` -- `csharp` - -## Generated Output Structure - -Each example generates output following this pattern: - -``` -example/ -├── README.md # Example-specific guide -├── generate.ts # Generation script -├── generated/ # Output (created after generation) -│ ├── index.ts/py/cs # Main exports -│ ├── resources/ # FHIR resources -│ ├── types/ # Complex types -│ └── enums/ # Value set enums -└── [language-specific files] # requirements.txt, .csproj, etc. -``` - -## Debug Output - -During type generation, you can inspect intermediate data structures to understand how FHIR is transformed into your target language: +To run the TypeScript R4 demo after generation: -### `writeTypeSchemas` - -```typescript -.writeTypeSchemas("./debug-schemas") +```bash +bun run examples/typescript-r4/demo.ts ``` -Exports the **TypeSchema** intermediate format as NDJSON files - the universal representation between FHIR and target languages. +For the Mustache example: -### `writeTypeTree` - -```typescript -.writeTypeTree("./dependency-tree.yaml") +```bash +bun run examples/mustache/mustache-java-r4-gen.ts ``` -Generates a **YAML file showing the dependency graph** - which types depend on which other types and their origin packages. +This generates a complete Maven project with Java classes ready to build. -## Integration Examples +## Prerequisites for C# Example -Add to your build pipeline +The C# example includes integration tests with Aidbox FHIR server. To run the tests: ```bash -#!/bin/bash -# scripts/generate-fhir-types.sh -cd codegen -bun run examples/typescript-r4/generate.ts -``` +# Start Aidbox server +docker compose up -```json -{ - "scripts": { - "generate:types": "bash scripts/generate-fhir-types.sh", - "prebuild": "npm run generate:types" - } -} +# In another terminal, run the C# tests +cd examples/csharp +dotnet test ``` -## Support - -For issues or questions: -- Check main [README.md](../README.md) -- Review [CONTRIBUTING.md](../CONTRIBUTING.md) -- Open an issue on GitHub +See [examples/csharp/README.md](csharp/README.md) for detailed setup instructions. diff --git a/examples/mustache/README.md b/examples/mustache/README.md new file mode 100644 index 000000000..11edcb658 --- /dev/null +++ b/examples/mustache/README.md @@ -0,0 +1,168 @@ +# Mustache Template-Based Generation Example + +Java code generation using Mustache templates and the FHIR R4 specification. + +## Overview + +This example demonstrates how to generate Java classes from FHIR R4 using template-based code generation. It includes: + +- Template-driven Java class generation with Mustache +- Automatic resource and complex type modeling +- Utility class generation for common operations +- Post-generation hooks for code formatting and testing +- Custom name transformations for Java conventions + +## Setup + +### Generate Java Types + +From the project root: + +```bash +cd codegen +bun install +bun run examples/mustache/mustache-java-r4-gen.ts +``` + +This will output to `./examples/mustache/mustache-java-r4-output/` + +### Build Java Project + +```bash +cd examples/mustache/mustache-java-r4-output +mvn clean package +``` + +## Configuration + +The generation is configured via `java/config.json`. Key settings include: + +**Type Mapping:** +Maps FHIR primitive types to Java types: +- `boolean` → `Boolean` +- `date` → `String` +- `dateTime` → `OffsetDateTime` +- `decimal` → `BigDecimal` +- `integer` → `Integer` + +**Name Transformations:** +Applies Java naming conventions: +- Enum values: `<=` → `LESS_OR_EQUAL` +- Types: Add `DTO` suffix +- Fields: Rename reserved words + +**Post-Generation Hooks:** +- Run Spotless for code formatting +- Execute Maven tests + +## Template Structure + +### Templates + +Located in `java/templates/`: + +- `model/resource_or_complex_type.mustache` - Main template for resources and complex types +- `model/utils/*.mustache` - Utility class templates +- `annotated_type.mustache` - Type with annotations +- `plain_type.mustache` - Simple type definition +- `primitive_wrapped_plain_type.mustache` - Wrapped primitive types + +### Static Files + +Located in `java/static/`: + +- `pom.xml` - Maven project configuration +- `model/src/` - Base Java project structure + +## Using Generated Types + +### Create a Resource + +```java +Patient patient = new Patient() + .setResourceType("Patient") + .setId("patient-1") + .setGender("male"); +``` + +### Add Extensions + +```java +patient.getExtension().add(new Extension() + .setUrl("http://example.com/extension") + .setValue(new StringType("value"))); +``` + +### Serialization + +```java +ObjectMapper mapper = new ObjectMapper(); +String json = mapper.writeValueAsString(patient); +Patient deserialized = mapper.readValue(json, Patient.class); +``` + +## Customization + +### Change Output Package + +Edit `java/config.json` and update the `package` property in renderings: + +```json +"properties": { + "package": "com.mycompany.fhir.models" +} +``` + +### Filter Specific Resources + +Customize which resources to generate via whitelist/blacklist in config: + +```json +"filters": { + "resource": { + "whitelist": ["Patient", "Observation"] + } +} +``` + +### Add Post-Generation Steps + +Add hooks to run additional tools after generation: + +```json +"hooks": { + "afterGenerate": [ + {"cmd": "mvn", "args": ["clean", "compile"]} + ] +} +``` + +## Template Variables + +Templates have access to the following context: + +**TypeViewModel:** +- `name` - Resource or type name +- `saveName` - Name safe for use as Java class name +- `description` - FHIR description +- `baseType` - Parent type if any +- `fields` - Array of fields + +**FieldViewModel:** +- `name` - Field name +- `saveName` - Safe field name +- `type` - Field type +- `isArray` - Whether field is a list +- `required` - Whether field is required +- `description` - Field description + +**Special Variables:** +- `meta.timestamp` - Generation timestamp +- `meta.generator` - Generator identification +- `properties` - Custom properties from config + +## Next Steps + +- See [examples/](../) overview for other language examples +- Check [../../docs/guides/mustache-generator.md](../../docs/guides/mustache-generator.md) for detailed Mustache template documentation +- Review `java/templates/` for template examples \ No newline at end of file diff --git a/examples/mustache/java/config.json b/examples/mustache/java/config.json new file mode 100644 index 000000000..431a45a7b --- /dev/null +++ b/examples/mustache/java/config.json @@ -0,0 +1,183 @@ +{ + "debug": "OFF", + "hooks": { + "afterGenerate": [ + {"cmd": "mvn","args": ["spotless:apply","--batch-mode", "--no-transfer-progress"]}, + {"cmd": "mvn","args": ["clean","test", "--batch-mode","--no-transfer-progress"]} + ] + }, + "renderings": { + "utility": [ + { + "source":"model/utils/utils.mustache", + "path": "model/src/main/java/de/solutio/fhir/models/", + "fileNameFormat":"Utils.java", + "properties": { + "package": "de.solutio.fhir.models" + } + }, + { + "source":"model/utils/resource_names.mustache", + "path": "model/src/main/java/de/solutio/fhir/models", + "fileNameFormat":"ResourceName.java", + "properties": { + "package": "de.solutio.fhir.models", + "resourcePackage": "de.solutio.fhir.models.resources" + } + }, + { + "source":"model/utils/primitive.mustache", + "path": "model/src/main/java/de/solutio/fhir/models", + "fileNameFormat":"Primitive.java", + "properties": { + "package": "de.solutio.fhir.models", + "complexTypePackage": "de.solutio.fhir.models.complex_types" + } + } + ], + "resource": [ + { + "source":"model/resource_or_complex_type.mustache", + "path": "model/src/main/java/de/solutio/fhir/models/resources", + "fileNameFormat":"%s.java", + "properties": { + "package": "de.solutio.fhir.models.resources", + "complexTypePackage": "de.solutio.fhir.models.complex_types", + "resourcePackage": "de.solutio.fhir.models.resources", + "additional_imports": [ + "de.solutio.fhir.models.Primitive", + "de.solutio.fhir.models.ResourceName", + "de.solutio.fhir.models.Utils" + ], + "features": { + "hash_code_equals": true, + "builder": true, + "to_string": true + } + } + } + ], + "complexType": [ + { + "source":"model/resource_or_complex_type.mustache", + "path": "model/src/main/java/de/solutio/fhir/models/complex_types", + "fileNameFormat":"%s.java", + "properties": { + "package": "de.solutio.fhir.models.complex_types", + "complexTypePackage": "de.solutio.fhir.models.complex_types", + "resourcePackage": "de.solutio.fhir.models.resources", + "additional_imports": [ + "de.solutio.fhir.models.Primitive", + "de.solutio.fhir.models.ResourceName", + "de.solutio.fhir.models.Utils" + ], + "features": { + "hash_code_equals": true, + "builder": true, + "to_string": true + } + } + } + ] + }, + "nameTransformations": { + "common": [ + {"pattern": "^(?\\d+)(?.*)$", "format": "_$$"} + ], + "enumValue": [ + {"pattern": "^<=$", "format": "LESS_OR_EQUAL"}, + {"pattern": "^>=$", "format": "GREATER_OR_EQUAL"}, + {"pattern": "^<$", "format": "LESS"}, + {"pattern": "^=$", "format": "EQUAL"}, + {"pattern": "^>$", "format": "GREATER"} + ], + "type": [ + {"pattern": "^(?.*)$", "format": "$DTO"} + ], + "field": [ + {"pattern": "^addItem$", "format": "addedItem"}, + {"pattern": "^for$", "format": "forReference"} + ] + }, + "keywords": [ + "abstract", + "assert", + "boolean", + "break", + "byte", + "case", + "catch", + "char", + "class", + "const", + "continue", + "default", + "do", + "double", + "else", + "enum", + "extends", + "final", + "finally", + "float", + "for", + "goto", + "if", + "implements", + "import", + "instanceof", + "int", + "interface", + "long", + "native", + "new", + "package", + "private", + "protected", + "public", + "return", + "short", + "static", + "strictfp", + "super", + "switch", + "synchronized", + "this", + "throw", + "throws", + "transient", + "try", + "void", + "volatile", + "while", + "true", + "false", + "null" + ], + "primitiveTypeMap":{ + "boolean": "Boolean", + "instant": "OffsetDateTime", + "time": "LocalTime", + "date": "String", + "dateTime": "OffsetDateTime", + + "decimal": "BigDecimal", + "integer": "Integer", + "unsignedInt": "Long", + "positiveInt": "Long", + "integer64": "Long", + "base64Binary": "String", + + "uri": "URI", + "url": "URI", + "canonical": "String", + "oid": "String", + "uuid": "UUID", + + "string": "String", + "code": "String", + "markdown": "String", + "id": "String", + "xhtml": "String" + } +} diff --git a/examples/mustache/java/static/model/pom.xml b/examples/mustache/java/static/model/pom.xml new file mode 100644 index 000000000..4ac75f43f --- /dev/null +++ b/examples/mustache/java/static/model/pom.xml @@ -0,0 +1,70 @@ + + + 4.0.0 + + de.solutio.fhir + sdk-parent + 0.0.0-SNAPSHOT + ../pom.xml + + model + > ${project.artifactId} + + + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + com.fasterxml.jackson.core + jackson-annotations + + + + + jakarta.annotation + jakarta.annotation-api + + + jakarta.validation + jakarta.validation-api + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.assertj + assertj-core + test + + + + + + + com.diffplug.spotless + spotless-maven-plugin + + + org.apache.maven.plugins + maven-source-plugin + + + + diff --git a/examples/mustache/java/static/model/src/test/java/de/solutio/fhir/models/resources/ResourceTest.java b/examples/mustache/java/static/model/src/test/java/de/solutio/fhir/models/resources/ResourceTest.java new file mode 100644 index 000000000..07cfd460e --- /dev/null +++ b/examples/mustache/java/static/model/src/test/java/de/solutio/fhir/models/resources/ResourceTest.java @@ -0,0 +1,69 @@ +package de.solutio.fhir.models.resources; +import static de.solutio.fhir.models.complex_types.CodeableConceptDTO.codeableConceptDtoBuilder; +import static de.solutio.fhir.models.complex_types.ElementDTO.elementDtoBuilder; +import static de.solutio.fhir.models.complex_types.ExtensionDTO.extensionDtoBuilder; +import static de.solutio.fhir.models.complex_types.ReferenceDTO.referenceDtoBuilder; +import static de.solutio.fhir.models.resources.TaskDTO.TaskInputDTO.taskInputDtoBuilder; +import static de.solutio.fhir.models.resources.TaskDTO.TaskIntentDTO; +import static de.solutio.fhir.models.resources.TaskDTO.TaskStatusDTO; +import static de.solutio.fhir.models.resources.TaskDTO.taskDtoBuilder; +import static org.assertj.core.api.Assertions.assertThat; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import de.solutio.fhir.models.ResourceName; +import de.solutio.fhir.models.complex_types.ReferenceDTO; +import java.net.URI; +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; +import org.junit.jupiter.api.Test; +class ResourceTest { + @Test + public void test() throws JsonProcessingException { + final ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.findAndRegisterModules(); + final TaskDTO task = taskDtoBuilder() + .description("some description") + .requester(referenceDtoBuilder().reference("123").type(URI.create("Practitioner")).build()) + .forReference(referenceDtoBuilder().reference("Patient/123").build()) + .addInsurance(referenceDtoBuilder().reference(ResourceName.ORGANIZATION_DTO, "123").build()) + .addBasedOn(referenceDtoBuilder().reference(ResourceName.TASK_DTO, UUID.randomUUID()).build()) + .authoredOn( + LocalDateTime.now(), + elementDtoBuilder() + .extension( + extensionDtoBuilder().url(URI.create("https://www.google.com")).build()) + .build()) + .intent(TaskIntentDTO.PLAN) + .status(TaskStatusDTO.REQUESTED) + .input(taskInputDtoBuilder().type(codeableConceptDtoBuilder().build()).build()) + .build(); + assertThat(task.toString()).isNotBlank(); + final String json = objectMapper.writeValueAsString(task); + assertThat(json).isNotBlank(); + final TaskDTO read = objectMapper.readValue(json, TaskDTO.class); + assertThat(read).isEqualTo(task); + assertThat(read.toString()).isNotBlank(); + final TaskDTO taskWithExtension = task.toBuilder() + ._description( + elementDtoBuilder() + .extension( + extensionDtoBuilder().url(URI.create("https://www.google.com")).build()) + .build()) + .extension(extensionDtoBuilder().url(URI.create("https://www.google.com")).build()) + .build(); + assertThat(taskWithExtension.description()).isEqualTo(task.description()); + assertThat(taskWithExtension.intent()).isEqualTo(task.intent()); + assertThat(taskWithExtension.status()).isEqualTo(task.status()); + assertThat(taskWithExtension.toString()).isNotBlank(); + final String jsonWithExtension = objectMapper.writeValueAsString(taskWithExtension); + final TaskDTO readWithExtension = objectMapper.readValue(jsonWithExtension, TaskDTO.class); + assertThat(readWithExtension).isEqualTo(taskWithExtension); + assertThat(readWithExtension.toString()).isNotBlank(); + assertThat(readWithExtension.forReference().flatMap(ReferenceDTO::findReferencedResourceName)).hasValue(ResourceName.PATIENT_DTO); + assertThat(readWithExtension.requester().flatMap(ReferenceDTO::findReferencedResourceName)).hasValue(ResourceName.PRACTITIONER_DTO); + assertThat(readWithExtension.insurance().map(List::getFirst).flatMap(ReferenceDTO::findReferencedResourceName)).hasValue(ResourceName.ORGANIZATION_DTO); + assertThat(readWithExtension.basedOn().map(List::getFirst).flatMap(ReferenceDTO::findReferencedResourceName)).hasValue(ResourceName.TASK_DTO); + } +} \ No newline at end of file diff --git a/examples/mustache/java/static/pom.xml b/examples/mustache/java/static/pom.xml new file mode 100644 index 000000000..86858fd0a --- /dev/null +++ b/examples/mustache/java/static/pom.xml @@ -0,0 +1,140 @@ + + + 4.0.0 + de.solutio.fhir + sdk-parent + 0.0.0-SNAPSHOT + pom + + + 21 + 21 + 21 + UTF-8 + UTF-8 + + + 2.19.4 + 5.14.1 + 3.27.6 + + 3.0.0 + 3.1.1 + + + 3.0.0 + 3.3.1 + + + + model + + + + + + + de.solutio.fhir + model + ${project.version} + + + de.solutio.fhir + client-spring + ${project.version} + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson.version} + + + + + jakarta.annotation + jakarta.annotation-api + ${jakarta.annotation-api} + + + jakarta.validation + jakarta.validation-api + ${jakarta.validation-api} + + + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + org.assertj + assertj-core + ${assertj.version} + test + + + + + + + + + com.diffplug.spotless + spotless-maven-plugin + ${spotless-maven-plugin.version} + + + + + collapse all blank lines + (?m)(\r?\n){2,} + $1 + + + + + + + + + + + + org.apache.maven.plugins + maven-source-plugin + ${maven-source-plugin.version} + + + attach-sources + verify + + jar-no-fork + + + + + + + + diff --git a/examples/mustache/java/templates/annotated_type.mustache b/examples/mustache/java/templates/annotated_type.mustache new file mode 100644 index 000000000..02d490b77 --- /dev/null +++ b/examples/mustache/java/templates/annotated_type.mustache @@ -0,0 +1,5 @@ +{{#isRequired}}@NotNull {{/isRequired}}{{^isRequired}}@Nullable {{/isRequired}} +{{^isArray}}{{#min}}@Min({{{min}}}){{/min}}{{#max}}@Max({{{max}}}){{/max}}{{/isArray}} +{{#isArray}}{{#isSizeConstrained}}@Size({{#min}}{{#max}}min={{{min}}},max={{{max}}}{{/max}}{{/min}}{{#min}}{{^max}}min={{{min}}}{{/max}}{{/min}}{{^min}}{{#max}}max={{{max}}}{{/max}}{{/min}}){{/isSizeConstrained}}{{/isArray}} +{{#isPrimitive.isDateTime}}@JsonFormat(shape = JsonFormat.Shape.STRING){{/isPrimitive.isDateTime}}{{#isPrimitive.isInstant}}@JsonFormat(shape = JsonFormat.Shape.STRING){{/isPrimitive.isInstant}} +@JsonProperty("{{{name}}}") {{#isArray}}List<{{/isArray}}{{#isResource}}@Valid {{/isResource}}{{#isComplexType}}@Valid {{/isComplexType}}{{{typeName}}}{{#isArray}}>{{/isArray}} \ No newline at end of file diff --git a/examples/mustache/java/templates/model/additional_codeable_concept_behaviour.mustache b/examples/mustache/java/templates/model/additional_codeable_concept_behaviour.mustache new file mode 100644 index 000000000..041796807 --- /dev/null +++ b/examples/mustache/java/templates/model/additional_codeable_concept_behaviour.mustache @@ -0,0 +1,14 @@ +public final Optional<{{#lambda.saveTypeName}}Coding{{/lambda.saveTypeName}}> findFirstCodingBySystem(URI url){ + requireNonNull(url); + if(this.{{#lambda.saveFieldName}}coding{{/lambda.saveFieldName}} == null){ + return Optional.empty(); + } + return this.{{#lambda.saveFieldName}}coding{{/lambda.saveFieldName}}.stream().filter(e -> e.{{#lambda.saveFieldName}}system{{/lambda.saveFieldName}}().map(url::equals).orElse(false)).findFirst(); +} +public final List<{{#lambda.saveTypeName}}Coding{{/lambda.saveTypeName}}> findCodingsBySystem(URI url){ + requireNonNull(url); + if(this.{{#lambda.saveFieldName}}coding{{/lambda.saveFieldName}} == null){ + return List.of(); + } + return this.{{#lambda.saveFieldName}}coding{{/lambda.saveFieldName}}.stream().filter(e -> e.{{#lambda.saveFieldName}}system{{/lambda.saveFieldName}}().map(url::equals).orElse(false)).toList(); +} \ No newline at end of file diff --git a/examples/mustache/java/templates/model/additional_extension_behaviour.mustache b/examples/mustache/java/templates/model/additional_extension_behaviour.mustache new file mode 100644 index 000000000..8ac870317 --- /dev/null +++ b/examples/mustache/java/templates/model/additional_extension_behaviour.mustache @@ -0,0 +1,14 @@ +public final Optional<{{#lambda.saveTypeName}}Extension{{/lambda.saveTypeName}}> findFirstExtensionByURL(URI url){ + requireNonNull(url); + if(this.{{#lambda.saveFieldName}}extension{{/lambda.saveFieldName}} == null){ + return Optional.empty(); + } + return this.{{#lambda.saveFieldName}}extension{{/lambda.saveFieldName}}.stream().filter(e -> e.{{#lambda.saveFieldName}}url{{/lambda.saveFieldName}}().equals(url)).findFirst(); +} +public final List<{{#lambda.saveTypeName}}Extension{{/lambda.saveTypeName}}> findExtensionsByURL(URI url){ + requireNonNull(url); + if(this.{{#lambda.saveFieldName}}extension{{/lambda.saveFieldName}} == null){ + return List.of(); + } + return this.{{#lambda.saveFieldName}}extension{{/lambda.saveFieldName}}.stream().filter(e -> e.{{#lambda.saveFieldName}}url{{/lambda.saveFieldName}}().equals(url)).toList(); +} \ No newline at end of file diff --git a/examples/mustache/java/templates/model/additional_reference_behaviour.mustache b/examples/mustache/java/templates/model/additional_reference_behaviour.mustache new file mode 100644 index 000000000..db69b43b5 --- /dev/null +++ b/examples/mustache/java/templates/model/additional_reference_behaviour.mustache @@ -0,0 +1,23 @@ +public final Optional findReferencedResourceName() { + if(this.type != null){ + final String path = this.type.getPath(); + return ResourceName.find(path); + } + if(this.reference != null && this.reference.contains("/")){ + final String name = this.reference.substring(0,this.reference.lastIndexOf("/")); + return ResourceName.find(name); + } + return Optional.empty(); +} +public final Optional findReferencedId(){ + if(this.reference == null || !this.reference.contains("/")){ + return Optional.empty(); + } + return Optional.of(this.reference.substring(this.reference.lastIndexOf("/")+1)); +} +public final Optional findReferencedUuid(){ + if(this.reference == null || !this.reference.contains("/")){ + return Optional.empty(); + } + return Optional.of(this.reference.substring(this.reference.lastIndexOf("/")+1)).map(UUID::fromString); +} \ No newline at end of file diff --git a/examples/mustache/java/templates/model/features/feature_builder.mustache b/examples/mustache/java/templates/model/features/feature_builder.mustache new file mode 100644 index 000000000..93d55cdad --- /dev/null +++ b/examples/mustache/java/templates/model/features/feature_builder.mustache @@ -0,0 +1,61 @@ +public static {{{saveName}}}Builder {{#lambda.camelCase}}{{{saveName}}}{{/lambda.camelCase}}Builder() { + return new {{{saveName}}}Builder(); +} + +{{>model/features/feature_builder_abstract_builder.mustache}} +{{>model/features/feature_builder_builder.mustache}} + +private {{{saveName}}}({{{saveName}}}Builder builder) { + this((Abstract{{{saveName}}}Builder)requireNonNull(builder)); +} + +protected {{{saveName}}}(Abstract{{{saveName}}}Builder builder) { + {{#parents.0}}super(requireNonNull(builder));{{/parents.0}} + {{#fields}} + {{#isRequired}}requireNonNull(builder.{{{saveName}}}, "missing '{{{saveName}}}'");{{/isRequired}} + {{/fields}} + {{#fields}} + {{#isArray}} + {{#isPrimitive}} + this.{{{saveName}}} = {{^isRequired}}builder.{{{saveName}}} != null ? {{/isRequired}}builder.{{{saveName}}}.stream().map(Primitive::value).toList(){{^isRequired}} : null{{/isRequired}}; + this._{{{saveName}}} = {{^isRequired}}builder.{{{saveName}}} != null ? {{/isRequired}}builder.{{{saveName}}}.stream().map(Primitive::element).toList(){{^isRequired}} : null{{/isRequired}}; + {{/isPrimitive}} + {{^isPrimitive}} + this.{{{saveName}}} = {{^isRequired}}builder.{{{saveName}}} != null ? {{/isRequired}}List.copyOf(builder.{{{saveName}}}){{^isRequired}} : null{{/isRequired}}; + {{/isPrimitive}} + {{/isArray}} + {{^isArray}} + {{#isPrimitive}} + this.{{{saveName}}} = {{^isRequired}}builder.{{{saveName}}} != null ? {{/isRequired}}requireNonNull(builder.{{{saveName}}}.value()){{^isRequired}} : null{{/isRequired}}; + this._{{{saveName}}} = {{^isRequired}}builder.{{{saveName}}} != null ? {{/isRequired}} builder.{{{saveName}}}.element(){{^isRequired}} : null{{/isRequired}}; + {{/isPrimitive}} + {{^isPrimitive}} + this.{{{saveName}}} = builder.{{{saveName}}}; + {{/isPrimitive}} + {{/isArray}} + {{/fields}} +} +protected > B applyToBuilder(B builder) { + requireNonNull(builder); + {{#hasParents}}super.applyToBuilder(builder);{{/hasParents}} + {{#hasFields}} + builder + {{#fields}} + .{{{saveName}}}(this.{{{saveName}}}{{#isPrimitive}},this._{{{saveName}}}{{/isPrimitive}}) + {{/fields}}; + {{/hasFields}} + return builder; +} +{{#hasChildren}} + protected Abstract{{{saveName}}}Builder toBuilder() { + return this.to{{{saveName}}}Builder(); + } + public final {{{saveName}}}Builder to{{{saveName}}}Builder() { + return this.applyToBuilder({{{saveName}}}.{{#lambda.camelCase}}{{{saveName}}}{{/lambda.camelCase}}Builder()); + } +{{/hasChildren}} +{{^hasChildren}} + public final {{{saveName}}}Builder toBuilder() { + return this.applyToBuilder({{{saveName}}}.{{#lambda.camelCase}}{{{saveName}}}{{/lambda.camelCase}}Builder()); + } +{{/hasChildren}} \ No newline at end of file diff --git a/examples/mustache/java/templates/model/features/feature_builder_abstract_builder.mustache b/examples/mustache/java/templates/model/features/feature_builder_abstract_builder.mustache new file mode 100644 index 000000000..fe0592d2c --- /dev/null +++ b/examples/mustache/java/templates/model/features/feature_builder_abstract_builder.mustache @@ -0,0 +1,116 @@ + + +protected static abstract class Abstract{{{saveName}}}Builder> {{#hasParents}}extends Abstract{{{parents.0.saveName}}}Builder{{/hasParents}}{ + + {{#fields}} + private @Nullable {{>primitive_wrapped_plain_type}} {{{saveName}}} = null; + {{/fields}} + + protected Abstract{{{saveName}}}Builder() { + } + + {{#isComplexType.isReference}} + {{>model/features/feature_builder_abstract_builder_additional_resource_behaviour}} + {{/isComplexType.isReference}} + + {{#fields}} + {{#isArray}} + private {{>primitive_wrapped_plain_type}} {{{saveName}}}(){ + if(this.{{{saveName}}} == null) { + this.{{{saveName}}} = new ArrayList<>(); + } + return this.{{{saveName}}}; + } + private void clear{{#lambda.saveFieldName}}{{#lambda.pascalCase}}{{{saveName}}}{{/lambda.pascalCase}}{{/lambda.saveFieldName}}(){ + if(this.{{{saveName}}} != null) { + this.{{{saveName}}}.clear(); + } + } + {{#isPrimitive}} + public final B {{{saveName}}}(@Nullable {{{typeName}}} {{{saveName}}}) { + return this.{{{saveName}}}({{{saveName}}}, null); + } + public final B {{{saveName}}}(@Nullable {{{typeName}}} {{{saveName}}}, @Nullable {{#lambda.saveTypeName}}Element{{/lambda.saveTypeName}} {{{saveName}}}Element) { + return this.{{{saveName}}}({{{saveName}}} != null ? List.of({{{saveName}}}) : null, {{{saveName}}}Element != null ? List.of({{{saveName}}}Element) : null); + } + public final B {{{saveName}}}(@Nullable {{>plain_type}} {{{saveName}}}, @Nullable List<{{#lambda.saveTypeName}}Element{{/lambda.saveTypeName}}> {{{saveName}}}Element) { + this.clear{{#lambda.saveFieldName}}{{#lambda.pascalCase}}{{{saveName}}}{{/lambda.pascalCase}}{{/lambda.saveFieldName}}(); + return this.add{{#lambda.saveFieldName}}{{#lambda.pascalCase}}{{{saveName}}}{{/lambda.pascalCase}}{{/lambda.saveFieldName}}({{{saveName}}}, {{{saveName}}}Element); + } + public final B add{{#lambda.saveFieldName}}{{#lambda.pascalCase}}{{{saveName}}}{{/lambda.pascalCase}}{{/lambda.saveFieldName}}(@Nullable {{{typeName}}} {{{saveName}}}) { + return this.add{{#lambda.saveFieldName}}{{#lambda.pascalCase}}{{{saveName}}}{{/lambda.pascalCase}}{{/lambda.saveFieldName}}({{{saveName}}}, null); + } + public final B add{{#lambda.saveFieldName}}{{#lambda.pascalCase}}{{{saveName}}}{{/lambda.pascalCase}}{{/lambda.saveFieldName}}(@Nullable {{{typeName}}} {{{saveName}}}, @Nullable {{#lambda.saveTypeName}}Element{{/lambda.saveTypeName}} {{{saveName}}}Element) { + return this.add{{#lambda.saveFieldName}}{{#lambda.pascalCase}}{{{saveName}}}{{/lambda.pascalCase}}{{/lambda.saveFieldName}}({{{saveName}}} != null ? List.of({{{saveName}}}) : null, {{{saveName}}}Element != null ? List.of({{{saveName}}}Element) : null); + } + public final B add{{#lambda.saveFieldName}}{{#lambda.pascalCase}}{{{saveName}}}{{/lambda.pascalCase}}{{/lambda.saveFieldName}}(@Nullable {{>plain_type}} {{{saveName}}}, @Nullable List<{{#lambda.saveTypeName}}Element{{/lambda.saveTypeName}}> {{{saveName}}}Element) { + if({{{saveName}}} != null && {{{saveName}}}Element != null && {{{saveName}}}.size() != {{{saveName}}}Element.size()) { + throw new IllegalArgumentException("Size of {{{saveName}}}("+{{{saveName}}}.size()+") and {{{saveName}}}Element("+{{{saveName}}}Element.size()+") must be equal"); + }else if({{{saveName}}}Element != null){ + throw new IllegalArgumentException("{{{saveName}}} must not be null to set {{{saveName}}}Element"); + } + if ({{{saveName}}} != null) { + this.{{{saveName}}}().addAll(IntStream.range(0, {{{saveName}}}.size()) + .mapToObj(i -> Primitive.nullable({{{saveName}}}.get(i), {{{saveName}}}Element.get(i))) + .toList() + ); + } + return this.self(); + } + {{#isDateTime}} + {{>model/features/feature_builder_local_date_time}} + {{/isDateTime}} + {{#isInstant}} + {{>model/features/feature_builder_local_date_time}} + {{/isInstant}} + {{/isPrimitive}} + {{^isPrimitive}} + public final B {{{saveName}}}(@Nullable {{{typeName}}} {{{saveName}}}) { + return this.{{{saveName}}}({{{saveName}}} != null ? List.of({{{saveName}}}) : null); + } + public final B {{{saveName}}}(@Nullable {{>plain_type}} {{{saveName}}}) { + this.clear{{#lambda.saveFieldName}}{{#lambda.pascalCase}}{{{saveName}}}{{/lambda.pascalCase}}{{/lambda.saveFieldName}}(); + return this.add{{#lambda.saveFieldName}}{{#lambda.pascalCase}}{{{saveName}}}{{/lambda.pascalCase}}{{/lambda.saveFieldName}}({{{saveName}}}); + } + public final B add{{#lambda.saveFieldName}}{{#lambda.pascalCase}}{{{saveName}}}{{/lambda.pascalCase}}{{/lambda.saveFieldName}}(@Nullable {{{typeName}}} {{{saveName}}}) { + return this.add{{#lambda.saveFieldName}}{{#lambda.pascalCase}}{{{saveName}}}{{/lambda.pascalCase}}{{/lambda.saveFieldName}}({{{saveName}}} != null ? List.of({{{saveName}}}) : null); + } + public final B add{{#lambda.saveFieldName}}{{#lambda.pascalCase}}{{{saveName}}}{{/lambda.pascalCase}}{{/lambda.saveFieldName}}(@Nullable {{>plain_type}} {{{saveName}}}) { + if ({{{saveName}}} != null) { + this.{{{saveName}}}().addAll({{{saveName}}}); + } + return this.self(); + } + {{/isPrimitive}} + {{/isArray}} + {{^isArray}} + {{#isPrimitive}} + public final B {{{saveName}}}(@Nullable {{>plain_type}} {{{saveName}}}) { + return this.{{{saveName}}}({{{saveName}}}, null); + } + public final B _{{{saveName}}}(@Nullable {{#lambda.saveTypeName}}Element{{/lambda.saveTypeName}} {{{saveName}}}Element) { + requireNonNull(this.{{{saveName}}}, "{{{saveName}}} must not be null to set {{{saveName}}}Element"); + return this.{{{saveName}}}(this.{{{saveName}}}.value(), {{{saveName}}}Element); + } + public final B {{{saveName}}}(@Nullable {{>plain_type}} {{{saveName}}}, @Nullable {{#lambda.saveTypeName}}Element{{/lambda.saveTypeName}} {{{saveName}}}Element) { + this.{{{saveName}}} = {{{saveName}}} != null ? Primitive.nonNullable({{{saveName}}}, {{{saveName}}}Element) : null; + return this.self(); + } + {{#isDateTime}} + {{>model/features/feature_builder_local_date_time}} + {{/isDateTime}} + {{#isInstant}} + {{>model/features/feature_builder_local_date_time}} + {{/isInstant}} + {{/isPrimitive}} + {{^isPrimitive}} + public final B {{{saveName}}}(@Nullable {{>plain_type}} {{{saveName}}}) { + this.{{{saveName}}} = {{{saveName}}}; + return this.self(); + } + {{/isPrimitive}} + {{/isArray}} + {{/fields}} + + protected abstract B self(); +} \ No newline at end of file diff --git a/examples/mustache/java/templates/model/features/feature_builder_abstract_builder_additional_resource_behaviour.mustache b/examples/mustache/java/templates/model/features/feature_builder_abstract_builder_additional_resource_behaviour.mustache new file mode 100644 index 000000000..d5804f88b --- /dev/null +++ b/examples/mustache/java/templates/model/features/feature_builder_abstract_builder_additional_resource_behaviour.mustache @@ -0,0 +1,13 @@ +public final B reference(ResourceName resourceName, UUID id){ + requireNonNull(resourceName, "resourceName must not be null"); + requireNonNull(id, "id must not be null"); + return this.reference(resourceName, id.toString()); +} +public final B reference(ResourceName resourceName, String id){ + requireNonNull(resourceName, "resourceName must not be null"); + requireNonNull(id, "id must not be null"); + if(id.isBlank()){ + throw new IllegalArgumentException("id must not be blank"); + } + return this.reference(resourceName.typeName()+"/"+id.trim().strip()).type(resourceName.uri()); +} \ No newline at end of file diff --git a/examples/mustache/java/templates/model/features/feature_builder_builder.mustache b/examples/mustache/java/templates/model/features/feature_builder_builder.mustache new file mode 100644 index 000000000..76ba9fc78 --- /dev/null +++ b/examples/mustache/java/templates/model/features/feature_builder_builder.mustache @@ -0,0 +1,10 @@ +public static final class {{{saveName}}}Builder extends Abstract{{{saveName}}}Builder<{{{saveName}}},{{{saveName}}}Builder>{ + private {{{saveName}}}Builder(){ + } + protected {{{saveName}}}Builder self(){ + return this; + } + public {{{saveName}}} build(){ + return new {{{saveName}}}(this); + } +} \ No newline at end of file diff --git a/examples/mustache/java/templates/model/features/feature_builder_local_date_time.mustache b/examples/mustache/java/templates/model/features/feature_builder_local_date_time.mustache new file mode 100644 index 000000000..3aaffec0c --- /dev/null +++ b/examples/mustache/java/templates/model/features/feature_builder_local_date_time.mustache @@ -0,0 +1,13 @@ +public final B {{{saveName}}}(@Nullable LocalDateTime {{{saveName}}}) { + return this.{{{saveName}}}({{{saveName}}}, null); +} +{{#isArray}} + public final B {{{saveName}}}(@Nullable LocalDateTime {{{saveName}}}, @Nullable {{#lambda.saveTypeName}}Element{{/lambda.saveTypeName}} {{{saveName}}}Element) { + return this.{{{saveName}}}({{{saveName}}} != null ? List.of({{{saveName}}}.atZone(ZoneId.systemDefault()).toOffsetDateTime()) : null, {{{saveName}}}Element != null ? List.of({{{saveName}}}Element) : null); + } +{{/isArray}} +{{^isArray}} + public final B {{{saveName}}}(@Nullable LocalDateTime {{{saveName}}}, @Nullable {{#lambda.saveTypeName}}Element{{/lambda.saveTypeName}} {{{saveName}}}Element) { + return this.{{{saveName}}}({{{saveName}}} != null ? {{{saveName}}}.atZone(ZoneId.systemDefault()).toOffsetDateTime() : null, {{{saveName}}}Element); + } +{{/isArray}} \ No newline at end of file diff --git a/examples/mustache/java/templates/model/features/feature_hashcode_equals.mustache b/examples/mustache/java/templates/model/features/feature_hashcode_equals.mustache new file mode 100644 index 000000000..7edf8ed11 --- /dev/null +++ b/examples/mustache/java/templates/model/features/feature_hashcode_equals.mustache @@ -0,0 +1,23 @@ +public int hashCode() { + return Objects.hash({{#hasParents}}super.hashCode(){{#hasFields}},{{/hasFields}}{{/hasParents}}{{#fields}}this.{{{saveName}}}{{^-last}}, {{/-last}}{{/fields}}); +} +public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (obj == null) { + return false; + {{#hasParents}} + }else if(!super.equals(obj)) { + return false; + {{/hasParents}} + } else if (!(obj instanceof {{{saveName}}})) { + return false; + } + {{#hasFields}} + final {{{saveName}}} other = ({{{saveName}}}) obj; + return {{#fields}}Utils.equals(this.{{{saveName}}}, other.{{{saveName}}}){{^-last}} && {{/-last}}{{/fields}}; + {{/hasFields}} + {{^hasFields}} + return true; + {{/hasFields}} +} \ No newline at end of file diff --git a/examples/mustache/java/templates/model/features/feature_to_string.mustache b/examples/mustache/java/templates/model/features/feature_to_string.mustache new file mode 100644 index 000000000..3f048a09a --- /dev/null +++ b/examples/mustache/java/templates/model/features/feature_to_string.mustache @@ -0,0 +1,20 @@ +protected Utils.ToStringHelper toStringHelper(){ + return {{#hasParents}}super.toStringHelper(){{/hasParents}}{{^hasParents}}Utils.toStringHelper(this){{/hasParents}} + {{#fields}} + .add("{{saveName}}", this.{{saveName}}) + {{#isPrimitive}} + .add("_{{saveName}}", this._{{saveName}}) + {{/isPrimitive}} + {{/fields}}; +} + +{{#isResource.isResource}} + public final String toString(){ + return this.toStringHelper().toString(); + } +{{/isResource.isResource}} +{{#isComplexType.isElement}} + public final String toString(){ + return this.toStringHelper().toString(); + } +{{/isComplexType.isElement}} \ No newline at end of file diff --git a/examples/mustache/java/templates/model/field_getter.mustache b/examples/mustache/java/templates/model/field_getter.mustache new file mode 100644 index 000000000..73528a58b --- /dev/null +++ b/examples/mustache/java/templates/model/field_getter.mustache @@ -0,0 +1,14 @@ +public {{^isRequired}}Optional<{{/isRequired}}{{>plain_type}}{{^isRequired}}>{{/isRequired}} {{#lambda.saveFieldName}}{{#lambda.camelCase}}{{{saveName}}}{{/lambda.camelCase}}{{/lambda.saveFieldName}}() { + return {{^isRequired}}Optional.ofNullable({{/isRequired}}this.{{{saveName}}}{{^isRequired}}){{/isRequired}}; +} +{{#isPrimitive}} + public Optional<{{#isArray}}List<{{/isArray}}{{#lambda.saveTypeName}}Element{{/lambda.saveTypeName}}>{{#isArray}}>{{/isArray}} _{{#lambda.saveFieldName}}{{#lambda.camelCase}}{{{saveName}}}{{/lambda.camelCase}}{{/lambda.saveFieldName}}() { + return Optional.ofNullable(this._{{{saveName}}}); + } + {{#isDateTime}} + {{>model/field_getter_local_date_time}} + {{/isDateTime}} + {{#isInstant}} + {{>model/field_getter_local_date_time}} + {{/isInstant}} +{{/isPrimitive}} \ No newline at end of file diff --git a/examples/mustache/java/templates/model/field_getter_local_date_time.mustache b/examples/mustache/java/templates/model/field_getter_local_date_time.mustache new file mode 100644 index 000000000..3317dcedc --- /dev/null +++ b/examples/mustache/java/templates/model/field_getter_local_date_time.mustache @@ -0,0 +1,10 @@ +{{^isArray}} + public {{^isRequired}}Optional<{{/isRequired}}LocalDateTime{{^isRequired}}>{{/isRequired}} {{#lambda.saveFieldName}}{{#lambda.camelCase}}{{{saveName}}}{{/lambda.camelCase}}AsLocalDateTime{{/lambda.saveFieldName}}() { + return {{^isRequired}}Optional.ofNullable({{/isRequired}}this.{{{saveName}}}{{^isRequired}}).map(odt->odt{{/isRequired}}.atZoneSameInstant(ZoneId.systemDefault()).toLocalDateTime(){{^isRequired}}){{/isRequired}}; + } +{{/isArray}} +{{#isArray}} + public {{^isRequired}}Optional<{{/isRequired}}List{{^isRequired}}>{{/isRequired}} {{#lambda.saveFieldName}}{{#lambda.camelCase}}{{{saveName}}}{{/lambda.camelCase}}AsLocalDateTime{{/lambda.saveFieldName}}() { + return {{^isRequired}}Optional.ofNullable({{/isRequired}}this.{{{saveName}}}{{^isRequired}}).map(odts->odts{{/isRequired}}.stream().map(odt->odt.atZoneSameInstant(ZoneId.systemDefault()).toLocalDateTime()).toList(){{^isRequired}}){{/isRequired}}; + } +{{/isArray}} \ No newline at end of file diff --git a/examples/mustache/java/templates/model/inner_enum.mustache b/examples/mustache/java/templates/model/inner_enum.mustache new file mode 100644 index 000000000..c14e94d06 --- /dev/null +++ b/examples/mustache/java/templates/model/inner_enum.mustache @@ -0,0 +1,37 @@ +public enum {{{saveName}}} { +{{#values}} + {{#lambda.upperCase}}{{{saveName}}}{{/lambda.upperCase}}("{{{name}}}"){{^-last}},{{/-last}} +{{/values}}; + + private static final Map BY_VALUE; + static { + final Map builder = new HashMap<>(); + for ({{{saveName}}} c: {{{saveName}}}.values()) { + builder.put(c.value, c); + } + BY_VALUE = Collections.unmodifiableMap(builder); + } + @JsonCreator + public static {{{saveName}}} fromValue(String value) { + requireNonNull(value); + if({{{saveName}}}.BY_VALUE.containsKey(value)) { + return BY_VALUE.get(value); + } + throw new IllegalArgumentException("Unexpected value '" + value + "'"); + } + + private final String value; + {{{saveName}}}(String value) { + this.value = requireNonNull(value); + } + + @JsonValue + public String getValue() { + return value; + } + + @Override + public String toString() { + return String.valueOf(value); + } +} \ No newline at end of file diff --git a/examples/mustache/java/templates/model/inner_resource_or_complex_type.mustache b/examples/mustache/java/templates/model/inner_resource_or_complex_type.mustache new file mode 100644 index 000000000..32db1c133 --- /dev/null +++ b/examples/mustache/java/templates/model/inner_resource_or_complex_type.mustache @@ -0,0 +1,79 @@ +{{#isResource}} +@JsonTypeName("{{{name}}}") +{{#children.0}} +@JsonSubTypes({ +{{#children}} + @JsonSubTypes.Type(value = {{{saveName}}}.class){{^-last}},{{/-last}} +{{/children}} +}) +{{/children.0}}{{/isResource}} +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE, creatorVisibility = JsonAutoDetect.Visibility.NONE, scalarConstructorVisibility = JsonAutoDetect.Visibility.NONE) +public {{#isNested}}static{{/isNested}} class {{{saveName}}} {{#parents.0}}extends {{{parents.0.saveName}}}{{/parents.0}}{ + {{#isResource}} + public static final ResourceName NAME = ResourceName.{{#lambda.upperCase}}{{#lambda.snakeCase}}{{saveName}}{{/lambda.snakeCase}}{{/lambda.upperCase}}; + {{/isResource}} + + {{#nestedComplexTypes}} + {{>model/inner_resource_or_complex_type}} + {{/nestedComplexTypes}} + + {{#nestedEnums}} + {{>model/inner_enum}} + {{/nestedEnums}} + + {{#fields}} + private final {{>annotated_type}} {{{saveName}}}; + {{#isPrimitive}} + private final @Nullable @JsonProperty("_{{{name}}}") {{#isArray}}List<{{/isArray}}{{#lambda.saveTypeName}}Element{{/lambda.saveTypeName}}{{#isArray}}>{{/isArray}} _{{{saveName}}}; + {{/isPrimitive}} + {{/fields}} + + protected {{{saveName}}}(){ + {{#fields}} + this.{{{saveName}}} = null; + {{#isPrimitive}} + this._{{{saveName}}} = null; + {{/isPrimitive}} + {{/fields}} + } + {{#isResource}} + public ResourceName resourceName() { + return {{{saveName}}}.NAME; + } + public {{#lambda.saveTypeName}}Reference{{/lambda.saveTypeName}} toReference() { + return {{#lambda.saveTypeName}}Reference{{/lambda.saveTypeName}}.{{#lambda.camelCase}}{{#lambda.saveTypeName}}Reference{{/lambda.saveTypeName}}{{/lambda.camelCase}}Builder() + .reference(this.resourceName(), this.{{#lambda.saveFieldName}}id{{/lambda.saveFieldName}}().orElseThrow(() -> new IllegalStateException("missing id"))) + .build(); + } + {{/isResource}} + + {{#isResource.isDomainResource}} + {{>model/additional_extension_behaviour}} + {{/isResource.isDomainResource}} + {{#isComplexType.isElement}} + {{>model/additional_extension_behaviour}} + {{/isComplexType.isElement}} + {{#isComplexType.isCodeableConcept}} + {{>model/additional_codeable_concept_behaviour}} + {{/isComplexType.isCodeableConcept}} + {{#isComplexType.isReference}} + {{>model/additional_reference_behaviour}} + {{/isComplexType.isReference}} + + {{#fields}} + {{>model/field_getter}} + {{/fields}} + + {{#properties.features.builder}} + {{>model/features/feature_builder}} + {{/properties.features.builder}} + + {{#properties.features.hash_code_equals}} + {{>model/features/feature_hashcode_equals}} + {{/properties.features.hash_code_equals}} + + {{#properties.features.to_string}} + {{>model/features/feature_to_string}} + {{/properties.features.to_string}} +} \ No newline at end of file diff --git a/examples/mustache/java/templates/model/resource_or_complex_type.mustache b/examples/mustache/java/templates/model/resource_or_complex_type.mustache new file mode 100644 index 000000000..940387046 --- /dev/null +++ b/examples/mustache/java/templates/model/resource_or_complex_type.mustache @@ -0,0 +1,67 @@ +package {{{properties.package}}}; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Generated; +import jakarta.annotation.Nullable; +import jakarta.annotation.Generated; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import jakarta.validation.Valid; + +{{#properties.additional_imports}} +import {{{.}}}; +{{/properties.additional_imports}} + +{{#model.isResource}} + import {{{properties.complexTypePackage}}}.{{#lambda.saveTypeName}}Reference{{/lambda.saveTypeName}}; +{{/model.isResource}} +import {{{properties.complexTypePackage}}}.{{#lambda.saveTypeName}}Element{{/lambda.saveTypeName}}; + +{{#model.dependencies}} + {{#complexTypes}} + import {{{properties.complexTypePackage}}}.{{{saveName}}}; + {{/complexTypes}} + + {{#resources}} + import {{{properties.resourcePackage}}}.{{{saveName}}}; + {{/resources}} +{{/model.dependencies}} + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; + +import java.net.URI; +import java.math.BigDecimal; +import java.time.OffsetDateTime; +import java.time.LocalTime; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.List; +import java.util.UUID; +import java.util.Collections; +import java.util.Objects; +import java.util.Optional; +import java.util.HashMap; +import java.util.Map; +import java.util.Arrays; +import java.util.ArrayList; +import java.util.stream.IntStream; + +@Generated(value="{{{meta.generator}}}", date="{{{meta.timestamp}}}") +{{#model}} +{{#isResource.isResource}} +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "resourceType") +{{/isResource.isResource}} +{{>model/inner_resource_or_complex_type}} +{{/model}} \ No newline at end of file diff --git a/examples/mustache/java/templates/model/utils/primitive.mustache b/examples/mustache/java/templates/model/utils/primitive.mustache new file mode 100644 index 000000000..362b0e663 --- /dev/null +++ b/examples/mustache/java/templates/model/utils/primitive.mustache @@ -0,0 +1,40 @@ +package {{{properties.package}}}; + +import static java.util.Objects.requireNonNull; + +import {{{properties.complexTypePackage}}}.{{#lambda.saveTypeName}}Element{{/lambda.saveTypeName}}; +import jakarta.annotation.Nullable; + +public record Primitive ( + @Nullable T value, + @Nullable {{#lambda.saveTypeName}}Element{{/lambda.saveTypeName}} element, + boolean isNullable +){ + public static Primitive nullable(@Nullable V value){ + return new Primitive<>(value, null, true); + } + public static Primitive nullable(@Nullable V value, {{#lambda.saveTypeName}}Element{{/lambda.saveTypeName}} element){ + return new Primitive<>(value, element, true); + } + public static Primitive nonNullable(V value, {{#lambda.saveTypeName}}Element{{/lambda.saveTypeName}} element){ + requireNonNull(value); + return new Primitive<>(value, element, false); + } + public static Primitive nonNullable(V value){ + requireNonNull(value); + return new Primitive<>(value, null, false); + } + + public Primitive{ + if(!isNullable && value == null){ + throw new NullPointerException("missing 'value'"); + } + } + + public Primitive withElement( {{#lambda.saveTypeName}}Element{{/lambda.saveTypeName}} element){ + return new Primitive(this.value, element, this.isNullable); + } + public Primitive withValue(T value){ + return new Primitive(value, this.element, this.isNullable); + } +} \ No newline at end of file diff --git a/examples/mustache/java/templates/model/utils/resource_names.mustache b/examples/mustache/java/templates/model/utils/resource_names.mustache new file mode 100644 index 000000000..9ed8b11ce --- /dev/null +++ b/examples/mustache/java/templates/model/utils/resource_names.mustache @@ -0,0 +1,49 @@ +package {{{properties.package}}}; + +import static java.util.Objects.requireNonNull; +import jakarta.annotation.Generated; +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.net.URI; + +{{#model.resources}} + import {{{properties.resourcePackage}}}.{{{saveName}}}; +{{/model.resources}} + +@Generated(value="{{{meta.generator}}}", date="{{{meta.timestamp}}}") +{{#model}} +public enum ResourceName{ + {{#resources}} + {{#lambda.upperCase}}{{#lambda.snakeCase}}{{saveName}}{{/lambda.snakeCase}}{{/lambda.upperCase}}("{{{name}}}"){ + public Class<{{{saveName}}}> resourceClass() { + return {{{saveName}}}.class; + } + }{{^-last}},{{/-last}} + {{/resources}}; + + private static final Map BY_NAME = Arrays.stream(values()).collect(Collectors.toMap(ResourceName::typeName, Function.identity())); + + public static Optional find(String name) { + requireNonNull(name); + return Optional.ofNullable(BY_NAME.get(name)); + } + + private final String typeName; + private final URI uri; + ResourceName(String typeName) { + this.typeName = requireNonNull(typeName); + this.uri = URI.create(typeName); + } + public URI uri() { + return uri; + } + + public String typeName() { + return typeName; + } + public abstract Class resourceClass(); +} +{{/model}} \ No newline at end of file diff --git a/examples/mustache/java/templates/model/utils/utils.mustache b/examples/mustache/java/templates/model/utils/utils.mustache new file mode 100644 index 000000000..3655febfb --- /dev/null +++ b/examples/mustache/java/templates/model/utils/utils.mustache @@ -0,0 +1,77 @@ +package de.solutio.fhir.models; + +import jakarta.annotation.Nullable; +import java.time.OffsetDateTime; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import static java.util.Objects.requireNonNull; + +public enum Utils { + ; + + public static boolean equals(@Nullable Object a, @Nullable Object b) { + return Objects.equals(a, b); + } + + public static boolean equals(@Nullable List a, @Nullable List b) { + if (a == b) { + return true; + } + if (a == null || b == null) { + return false; + } + if (a.size() != b.size()) { + return false; + } + for (int i = 0; i < a.size(); i++) { + if (!Utils.equals(a.get(i), b.get(i))) { + return false; + } + } + return true; + } + + public static boolean equals(@Nullable OffsetDateTime a, @Nullable OffsetDateTime b) { + return (a == b) || (a != null && b != null && a.isEqual(b)); + } + + public static ToStringHelper toStringHelper(Class clazz) { + requireNonNull(clazz); + return new ToStringHelper(clazz.getSimpleName()); + } + public static ToStringHelper toStringHelper(Object object) { + requireNonNull(object); + return Utils.toStringHelper(object.getClass()); + } + + public static final class ToStringHelper { + private record Value(String name, Object value) { + public Value{ + requireNonNull(name); + } + } + private final String name; + private final List values; + + private ToStringHelper(String name) { + this.name = requireNonNull(name); + this.values = new LinkedList<>(); + } + + public ToStringHelper add(String name, @Nullable Object value) { + this.values.add(new Value(name, value)); + return this; + } + public String toString() { + return this.name + '{' + + this.values.stream() + .filter(value -> value.value() != null) + .map(value -> value.name() + '=' + value.value()) + .collect(Collectors.joining(", ")) + + '}'; + } + } +} \ No newline at end of file diff --git a/examples/mustache/java/templates/plain_type.mustache b/examples/mustache/java/templates/plain_type.mustache new file mode 100644 index 000000000..4394f951e --- /dev/null +++ b/examples/mustache/java/templates/plain_type.mustache @@ -0,0 +1 @@ +{{#isArray}}List<{{/isArray}}{{{typeName}}}{{#isArray}}>{{/isArray}} \ No newline at end of file diff --git a/examples/mustache/java/templates/primitive_wrapped_plain_type.mustache b/examples/mustache/java/templates/primitive_wrapped_plain_type.mustache new file mode 100644 index 000000000..0d34c5b29 --- /dev/null +++ b/examples/mustache/java/templates/primitive_wrapped_plain_type.mustache @@ -0,0 +1 @@ +{{#isArray}}List<{{/isArray}}{{#isPrimitive}}Primitive<{{/isPrimitive}}{{{typeName}}}{{#isPrimitive}}>{{/isPrimitive}}{{#isArray}}>{{/isArray}} \ No newline at end of file diff --git a/examples/mustache/mustache-java-r4-gen.ts b/examples/mustache/mustache-java-r4-gen.ts new file mode 100644 index 000000000..a4d48be2d --- /dev/null +++ b/examples/mustache/mustache-java-r4-gen.ts @@ -0,0 +1,25 @@ +import { APIBuilder } from "../../src/api/builder"; + +if (require.main === module) { + console.log("📦 Generating FHIR R4 Core Types..."); + + const builder = new APIBuilder() + .setLogLevel("DEBUG") + .throwException() + .fromPackage("hl7.fhir.r4.core", "4.0.1") + .mustache("./examples/mustache/java", { debug: "COMPACT" }) + .outputTo("./examples/mustache/mustache-java-r4-output") + .writeTypeTree("./examples/mustache/mustache-java-r4-output/type-tree.yaml") + .cleanOutput(true); + + const report = await builder.generate(); + + console.log(report); + + if (report.success) { + console.log("✅ FHIR R4 types generated successfully!"); + } else { + console.error("❌ FHIR R4 types generation failed."); + process.exit(1); + } +} diff --git a/examples/mustache/tsconfig.examples-mustache.json b/examples/mustache/tsconfig.examples-mustache.json new file mode 100644 index 000000000..bb542f383 --- /dev/null +++ b/examples/mustache/tsconfig.examples-mustache.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["."] +} diff --git a/package.json b/package.json index 10fc529fe..1a9b5bc71 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "dependencies": { "@atomic-ehr/fhir-canonical-manager": "canary", "@atomic-ehr/fhirschema": "^0.0.5", + "mustache": "^4.2.0", "picocolors": "^1.1.1", "tinyglobby": "^0.2.15", "yaml": "^2.8.2", @@ -75,6 +76,7 @@ "devDependencies": { "@biomejs/biome": "^2.3.9", "@types/bun": "^1.3.4", + "@types/mustache": "^4.2.6", "@types/node": "^22.19.3", "@types/yargs": "^17.0.35", "knip": "^5.73.4", diff --git a/src/api/builder.ts b/src/api/builder.ts index cbc6139cb..348afa35f 100644 --- a/src/api/builder.ts +++ b/src/api/builder.ts @@ -14,6 +14,13 @@ import { Python, type PythonGeneratorOptions } from "@root/api/writer-generator/ import { generateTypeSchemas } from "@root/typeschema"; import { registerFromManager } from "@root/typeschema/register"; import { type TreeShake, treeShake } from "@root/typeschema/tree-shake"; +import { + extractNameFromCanonical, + type PackageMeta, + packageMetaToFhir, + packageMetaToNpm, + type TypeSchema, +} from "@root/typeschema/types"; import { mkTypeSchemaIndex, type TypeSchemaIndex } from "@root/typeschema/utils"; import { type CodegenLogger, @@ -22,16 +29,12 @@ import { type LogLevelString, parseLogLevel, } from "@root/utils/codegen-logger"; -import { - extractNameFromCanonical, - type PackageMeta, - packageMetaToFhir, - packageMetaToNpm, - type TypeSchema, -} from "@typeschema/types"; +import type { PartialBy } from "@root/utils/types"; import type { TypeSchemaConfig } from "../config"; +import type { FileBasedMustacheGeneratorOptions } from "./writer-generator/mustache"; +import * as Mustache from "./writer-generator/mustache"; import { TypeScript, type TypeScriptOptions } from "./writer-generator/typescript"; -import type { FileBuffer, FileSystemWriter, WriterOptions } from "./writer-generator/writer"; +import type { FileBuffer, FileSystemWriter, FileSystemWriterOptions, WriterOptions } from "./writer-generator/writer"; /** * Configuration options for the API builder @@ -110,8 +113,6 @@ const normalizeFileName = (str: string): string => { return res; }; -export type PartialBy = Omit & Partial>; - type APIBuilderConfig = PartialBy< Required, "logger" | "typeSchemaConfig" | "typeSchemaOutputDir" | "exportTypeTree" | "treeShake" | "logLevel" | "registry" @@ -343,6 +344,28 @@ export class APIBuilder { return this; } + mustache(templatePath: string, userOpts: Partial) { + const defaultWriterOpts: FileSystemWriterOptions = { + logger: this.logger, + outputDir: this.options.outputDir, + }; + const defaultMustacheOpts: Partial = { + meta: { + timestamp: new Date().toISOString(), + generator: "atomic-codegen", + }, + }; + const opts = { + ...defaultWriterOpts, + ...defaultMustacheOpts, + ...userOpts, + }; + const generator = Mustache.createGenerator(templatePath, opts); + this.generators.set(`mustache[${templatePath}]`, generator); + this.logger.debug(`Configured TypeScript generator (${JSON.stringify(opts, undefined, 2)})`); + return this; + } + csharp(userOptions: Partial): APIBuilder { const defaultWriterOpts: WriterOptions = { logger: this.logger, diff --git a/src/api/mustache/generator/DebugMixinProvider.ts b/src/api/mustache/generator/DebugMixinProvider.ts new file mode 100644 index 000000000..0e7979f22 --- /dev/null +++ b/src/api/mustache/generator/DebugMixinProvider.ts @@ -0,0 +1,28 @@ +import type { DebugMixin } from "@mustache/types"; + +export class DebugMixinProvider { + constructor(private readonly mode: "FORMATTED" | "COMPACT") {} + + public apply>(target: T): T & DebugMixin { + return this._addDebug(target) as T & DebugMixin; + } + + private _addDebug(value: unknown): unknown { + if (Array.isArray(value)) { + return value.map((v) => this._addDebug(v)); + } + + if (value !== null && typeof value === "object") { + const obj = value as Record; + const result: Record = {}; + const debugString = JSON.stringify(obj, null, this.mode === "FORMATTED" ? 2 : undefined); + for (const [key, val] of Object.entries(obj)) { + result[key] = this._addDebug(val); + } + result.debug = debugString; + return result; + } + + return value; + } +} diff --git a/src/api/mustache/generator/LambdaMixinProvider.ts b/src/api/mustache/generator/LambdaMixinProvider.ts new file mode 100644 index 000000000..391a96148 --- /dev/null +++ b/src/api/mustache/generator/LambdaMixinProvider.ts @@ -0,0 +1,28 @@ +import type { NameGenerator } from "@mustache/generator/NameGenerator"; +import type { LambdaMixin } from "@mustache/types"; +import { camelCase, kebabCase, pascalCase, snakeCase } from "@root/api/writer-generator/utils"; + +export class LambdaMixinProvider { + private readonly lambda: LambdaMixin["lambda"]; + constructor(private readonly nameGenerator: NameGenerator) { + this.lambda = { + saveTypeName: () => (text, render) => this.nameGenerator.generateType(render(text)), + saveEnumValueName: () => (text, render) => this.nameGenerator.generateEnumValue(render(text)), + saveFieldName: () => (text, render) => this.nameGenerator.generateField(render(text)), + + camelCase: () => (text, render) => camelCase(render(text)), + snakeCase: () => (text, render) => snakeCase(render(text)), + pascalCase: () => (text, render) => pascalCase(render(text)), + kebabCase: () => (text, render) => kebabCase(render(text)), + lowerCase: () => (text, render) => render(text).toLowerCase(), + upperCase: () => (text, render) => render(text).toUpperCase(), + }; + } + + public apply>(target: T): T & LambdaMixin { + return { + ...target, + lambda: this.lambda, + }; + } +} diff --git a/src/api/mustache/generator/ListElementInformationMixinProvider.ts b/src/api/mustache/generator/ListElementInformationMixinProvider.ts new file mode 100644 index 000000000..084c8cf71 --- /dev/null +++ b/src/api/mustache/generator/ListElementInformationMixinProvider.ts @@ -0,0 +1,39 @@ +import type { ListElementInformationMixin } from "@mustache/types"; + +export class ListElementInformationMixinProvider { + private static _array(value: T[] | Set): T[] { + return Array.isArray(value) ? value : Array.from(value); + } + + public apply>(source: T): T { + return this._addListElementInformation(source) as T; + } + + private _addListElementInformation(value: unknown): unknown { + if (Array.isArray(value) || value instanceof Set) { + return ListElementInformationMixinProvider._array(value).map((v, index, array) => { + if (typeof v === "object" && v !== null) { + return { + ...(this._addListElementInformation(v) as Record), + "-index": index, + "-length": array.length, + "-first": index === 0, + "-last": index === array.length - 1, + } satisfies ListElementInformationMixin; + } + return v; + }); + } + + if (value !== null && typeof value === "object") { + const obj = value as Record; + const result: Record = {}; + for (const [key, val] of Object.entries(obj)) { + result[key] = this._addListElementInformation(val); + } + return result; + } + + return value; + } +} diff --git a/src/api/mustache/generator/NameGenerator.ts b/src/api/mustache/generator/NameGenerator.ts new file mode 100644 index 000000000..5719c51cb --- /dev/null +++ b/src/api/mustache/generator/NameGenerator.ts @@ -0,0 +1,115 @@ +import type { Field, Identifier, NestedType, TypeSchema } from "@typeschema/types"; + +export type NameTransformation = { + pattern: RegExp | string; + format: string; +}; +export type DistinctNameConfigurationType = { + common: T; + enumValue: T; + type: T; + field: T; +}; + +export class NameGenerator { + constructor( + private readonly keywords: Set, + private readonly typeMap: Record, + private readonly nameTransformations: DistinctNameConfigurationType, + private readonly unsaveCharacterPattern: string | RegExp, + ) {} + + private _replaceUnsaveChars(name: string): string { + const pattern = + this.unsaveCharacterPattern instanceof RegExp + ? this.unsaveCharacterPattern + : new RegExp(this.unsaveCharacterPattern, "g"); + return name.replace(pattern, "_"); + } + + private _applyNameTransformations(name: string, transformations: NameTransformation[]): string { + for (const transformation of this.nameTransformations.common) { + name = name.replace( + transformation.pattern instanceof RegExp + ? transformation.pattern + : new RegExp(transformation.pattern, "g"), + transformation.format, + ); + } + for (const transformation of transformations) { + name = name.replace( + transformation.pattern instanceof RegExp + ? transformation.pattern + : new RegExp(transformation.pattern, "g"), + transformation.format, + ); + } + return name; + } + + private _generateTypeName(name: string): string { + if (this.typeMap[name]) { + name = this.typeMap[name] as any; + } else { + name = this._applyNameTransformations(name, this.nameTransformations.type); + name = name.charAt(0).toUpperCase() + name.slice(1); + if (this.keywords.has(name)) { + return `_${name}`; + } + name = this._replaceUnsaveChars(name); + } + return name; + } + + public generateEnumType(name: string): string { + return this._generateTypeName(name); + } + + private _generateTypeFromTypeRef(typeRef: Identifier): string { + if (typeRef.kind === "primitive-type") { + return this._generateTypeName(typeRef.name); + } + return this._generateTypeName( + typeRef.url + .split("/") + .pop() + ?.split("#") + .map((s) => s.charAt(0).toUpperCase() + s.slice(1)) + .join("") ?? "", + ); + } + + public generateFieldType(schema: Field): string { + if ((schema as any).enum) { + return this.generateEnumType((schema as any).binding?.name ?? (schema as any).type.name); + } + return this._generateTypeFromTypeRef((schema as any).type); + } + + public generateType(schemaOrRefOrString: TypeSchema | NestedType | Identifier | string): string { + if (typeof schemaOrRefOrString === "string") { + return this._generateTypeName(schemaOrRefOrString); + } + if ("url" in schemaOrRefOrString) { + return this._generateTypeFromTypeRef(schemaOrRefOrString); + } + return this._generateTypeFromTypeRef(schemaOrRefOrString.identifier); + } + + public generateField(name: string): string { + name = this._applyNameTransformations(name, this.nameTransformations.field); + if (this.keywords.has(name)) { + return `_${name}`; + } + name = this._replaceUnsaveChars(name); + return name; + } + public generateEnumValue(name: string): string { + name = this._applyNameTransformations(name, this.nameTransformations.enumValue); + if (this.keywords.has(name)) { + return `_${name}`; + } + name = this._replaceUnsaveChars(name).toUpperCase(); + return name; + } +} diff --git a/src/api/mustache/generator/TemplateFileCache.ts b/src/api/mustache/generator/TemplateFileCache.ts new file mode 100644 index 000000000..92535277d --- /dev/null +++ b/src/api/mustache/generator/TemplateFileCache.ts @@ -0,0 +1,32 @@ +import fs from "node:fs"; +import Path from "node:path"; +import type { Rendering } from "@mustache/types"; + +export class TemplateFileCache { + private readonly templateBaseDir: string; + private readonly templateCache: Record = {}; + constructor(templateBaseDir: string) { + this.templateBaseDir = Path.resolve(templateBaseDir); + } + + private _normalizeName(name: string): string { + if (name.endsWith(".mustache")) { + return name; + } + return `${name}.mustache`; + } + + public read(template: Pick): string { + return this.readTemplate(template.source); + } + public readTemplate(name: string): string { + const normalizedName = this._normalizeName(name); + if (!this.templateCache[normalizedName]) { + this.templateCache[normalizedName] = fs.readFileSync( + Path.join(this.templateBaseDir, normalizedName), + "utf-8", + ); + } + return this.templateCache[normalizedName]; + } +} diff --git a/src/api/mustache/generator/ViewModelFactory.ts b/src/api/mustache/generator/ViewModelFactory.ts new file mode 100644 index 000000000..88ab050f6 --- /dev/null +++ b/src/api/mustache/generator/ViewModelFactory.ts @@ -0,0 +1,361 @@ +import { ListElementInformationMixinProvider } from "@mustache/generator/ListElementInformationMixinProvider"; +import type { NameGenerator } from "@mustache/generator/NameGenerator"; +import type { + EnumViewModel, + FieldViewModel, + NamedViewModel, + ResolvedTypeViewModel, + RootViewModel, + TypeViewModel, + ViewModel, +} from "@mustache/types"; +import { PRIMITIVE_TYPES } from "@mustache/types"; +import type { TypeSchemaIndex } from "@root/typeschema/utils"; +import type { IsPrefixed } from "@root/utils/types"; +import { + type Field, + type Identifier, + isComplexTypeIdentifier, + isResourceIdentifier, + type NestedType, + type TypeSchema, +} from "@typeschema/types"; + +export type ViewModelCache = { + resourcesByUri: Record; + complexTypesByUri: Record; +}; + +export class ViewModelFactory { + private arrayMixinProvider: ListElementInformationMixinProvider = new ListElementInformationMixinProvider(); + + constructor( + private readonly tsIndex: TypeSchemaIndex, + private readonly nameGenerator: NameGenerator, + private readonly filterPred: (id: Identifier) => boolean, + ) {} + + public createUtility(): RootViewModel { + return this._createForRoot(); + } + + public createComplexType( + typeRef: Identifier, + cache: ViewModelCache = { resourcesByUri: {}, complexTypesByUri: {} }, + ): RootViewModel { + const base = this._createForComplexType(typeRef, cache); + const parents = this._createParentsFor(base.schema, cache); + const children = this._createChildrenFor(typeRef, cache); + const inheritedFields = parents.flatMap((p) => p.fields); + return this.arrayMixinProvider.apply({ + ...this._createForRoot(), + ...base, + parents, + children, + inheritedFields, + allFields: [...base.fields, ...parents.flatMap((p) => p.fields)], + + hasChildren: children.length > 0, + hasParents: parents.length > 0, + hasInheritedFields: inheritedFields.length > 0, + }); + } + public createResource( + typeRef: Identifier, + cache: ViewModelCache = { resourcesByUri: {}, complexTypesByUri: {} }, + ): RootViewModel { + const base = this._createForResource(typeRef, cache); + const parents = this._createParentsFor(base.schema, cache); + const children = this._createChildrenFor(typeRef, cache); + const inheritedFields = parents.flatMap((p) => p.fields); + return this.arrayMixinProvider.apply({ + ...this._createForRoot(), + ...base, + parents, + children, + inheritedFields, + allFields: [...base.fields, ...inheritedFields], + + hasChildren: children.length > 0, + hasParents: parents.length > 0, + hasInheritedFields: inheritedFields.length > 0, + }); + } + + private _createFor(typeRef: Identifier, cache: ViewModelCache, nestedIn?: TypeSchema): TypeViewModel { + if (typeRef.kind === "complex-type") { + return this._createForComplexType(typeRef, cache, nestedIn); + } + if (typeRef.kind === "resource") { + return this._createForResource(typeRef, cache, nestedIn); + } + throw new Error(`Unknown type ${typeRef.kind}`); + } + + private _createForComplexType(typeRef: Identifier, cache: ViewModelCache, nestedIn?: TypeSchema): TypeViewModel { + const type = this.tsIndex.resolve(typeRef); + if (!type) { + throw new Error(`ComplexType ${typeRef.name} not found`); + } + if (!Object.hasOwn(cache.complexTypesByUri, type.identifier.url)) { + cache.complexTypesByUri[type.identifier.url] = this._createTypeViewModel(type, cache, nestedIn); + } + const res = cache.complexTypesByUri[type.identifier.url]; + if (!res) throw new Error(`ComplexType ${typeRef.name} not found`); + return res; + } + + private _createForResource(typeRef: Identifier, cache: ViewModelCache, nestedIn?: TypeSchema): TypeViewModel { + const type = this.tsIndex.resolve(typeRef); + if (!type) { + throw new Error(`Resource ${typeRef.name} not found`); + } + if (!Object.hasOwn(cache.resourcesByUri, type.identifier.url)) { + cache.resourcesByUri[type.identifier.url] = this._createTypeViewModel(type, cache, nestedIn); + } + const res = cache.resourcesByUri[type.identifier.url]; + if (!res) throw new Error(`Resource ${typeRef.name} not found`); + return res; + } + + private _createChildrenFor(typeRef: Identifier, cache: ViewModelCache, nestedIn?: TypeSchema): TypeViewModel[] { + if (isComplexTypeIdentifier(typeRef)) { + return this.tsIndex + .resourceChildren(typeRef) + .filter(isComplexTypeIdentifier) + .filter(this.filterPred) + .map((childRef: Identifier) => this._createFor(childRef, cache, nestedIn)); + } + if (isResourceIdentifier(typeRef)) { + return this.tsIndex + .resourceChildren(typeRef) + .filter(isResourceIdentifier) + .filter(this.filterPred) + .map((childRef: Identifier) => this._createFor(childRef, cache, nestedIn)); + } + return []; + } + + private _createParentsFor(base: TypeSchema | NestedType, cache: ViewModelCache) { + const parents: TypeViewModel[] = []; + let parentRef: Identifier | undefined = "base" in base ? base.base : undefined; + while (parentRef) { + parents.push(this._createFor(parentRef, cache, undefined)); + const parent = this.tsIndex.resolve(parentRef); + parentRef = parent && "base" in parent ? parent.base : undefined; + } + return parents; + } + + private _createForNestedType( + nested: NestedType, + cache: ViewModelCache, + nestedIn?: TypeSchema, + ): ResolvedTypeViewModel { + const base = this._createTypeViewModel(nested, cache, nestedIn); + const parents = this._createParentsFor(nested, cache); + const children = this._createChildrenFor(nested.identifier, cache, nestedIn); + const inheritedFields = parents.flatMap((p) => p.fields); + return { + ...base, + parents, + children, + inheritedFields, + allFields: [...base.fields, ...inheritedFields], + + hasChildren: children.length > 0, + hasParents: parents.length > 0, + hasInheritedFields: inheritedFields.length > 0, + }; + } + + private _createTypeViewModel( + schema: TypeSchema | NestedType, + cache: ViewModelCache, + nestedIn?: TypeSchema, + ): TypeViewModel { + const fields = Object.entries(("fields" in schema ? schema.fields : {}) ?? {}); + const nestedComplexTypes = this._collectNestedComplex(schema, cache); + const nestedEnums = this._collectNestedEnums(fields as [string, Field][]); + const dependencies = this._collectDependencies(schema); + const name: NamedViewModel = { + name: schema.identifier.name, + saveName: this.nameGenerator.generateType(schema), + }; + return { + nestedComplexTypes, + nestedEnums, + dependencies, + isNested: !!nestedIn, + schema: schema, + ...name, + isResource: this._createIsResource(schema.identifier), + isComplexType: this._createIsComplexType(schema.identifier), + + hasFields: fields.length > 0, + hasNestedComplexTypes: nestedComplexTypes.length > 0, + hasNestedEnums: nestedEnums.length > 0, + fields: fields + .filter(([_fieldName, fieldSchema]) => !!(fieldSchema as any).type) + .sort((a, b) => a[0].localeCompare(b[0])) + .map(([fieldName, fieldSchema]) => { + return { + owner: name, + schema: fieldSchema, + name: fieldName, + saveName: this.nameGenerator.generateField(fieldName), + typeName: this.nameGenerator.generateFieldType(fieldSchema as Field), + + isArray: (fieldSchema as any).array, + isRequired: (fieldSchema as any).required, + isEnum: !!(fieldSchema as any).enum, + + isSizeConstrained: + (fieldSchema as any).min !== undefined || (fieldSchema as any).max !== undefined, + min: (fieldSchema as any).min, + max: (fieldSchema as any).max, + + isResource: this._createIsResource((fieldSchema as any).type), + isComplexType: this._createIsComplexType((fieldSchema as any).type), + isPrimitive: this._createIsPrimitiveType((fieldSchema as any).type), + + isCode: (fieldSchema as any).type?.name === "code", + isIdentifier: (fieldSchema as any).type?.name === "Identifier", + isReference: (fieldSchema as any).type?.name === "Reference", + }; + }), + }; + } + + private _collectDependencies(schema: TypeSchema | NestedType): TypeViewModel["dependencies"] { + const dependencies: TypeViewModel["dependencies"] = { + resources: [], + complexTypes: [], + }; + if ("dependencies" in schema && schema.dependencies) { + schema.dependencies + .filter((dependency) => dependency.kind === "complex-type") + .map((dependency) => ({ name: dependency.name, saveName: this.nameGenerator.generateType(dependency) })) + .forEach((dependency) => { + dependencies.complexTypes.push(dependency); + }); + schema.dependencies + .filter((dependency) => dependency.kind === "resource") + .map((dependency) => ({ name: dependency.name, saveName: this.nameGenerator.generateType(dependency) })) + .forEach((dependency) => { + dependencies.resources.push(dependency); + }); + } + if ("nested" in schema && schema.nested) { + schema.nested + .map((nested) => this._collectDependencies(nested)) + .forEach((d) => { + d.complexTypes + .filter( + (complexType) => + !dependencies.complexTypes.some((dependency) => dependency.name === complexType.name), + ) + .forEach((complexType) => { + dependencies.complexTypes.push(complexType); + }); + d.resources + .filter( + (resource) => + !dependencies.resources.some((dependency) => dependency.name === resource.name), + ) + .forEach((resource) => { + dependencies.resources.push(resource); + }); + }); + } + return dependencies; + } + + private _createIsResource(typeRef: Identifier): Record, boolean> | false { + if (typeRef.kind !== "resource") { + return false; + } + return Object.fromEntries( + this.tsIndex + .collectResources() + .map((e) => e.identifier) + .map((resourceRef: Identifier) => [ + `is${resourceRef.name.charAt(0).toUpperCase() + resourceRef.name.slice(1)}`, + resourceRef.url === typeRef.url, + ]), + ) as Record, boolean>; + } + private _createIsComplexType(typeRef: Identifier): Record, boolean> | false { + if (typeRef.kind !== "complex-type" && typeRef.kind !== "nested") { + return false; + } + return Object.fromEntries( + this.tsIndex + .collectComplexTypes() + .map((e) => e.identifier) + .map((complexTypeRef: Identifier) => [ + `is${complexTypeRef.name.charAt(0).toUpperCase() + complexTypeRef.name.slice(1)}`, + complexTypeRef.url === typeRef.url, + ]), + ) as Record, boolean>; + } + private _createIsPrimitiveType(typeRef: Identifier): Record, boolean> | false { + if (typeRef.kind !== "primitive-type") { + return false; + } + return Object.fromEntries( + PRIMITIVE_TYPES.map((type) => [`is${type.charAt(0).toUpperCase()}${type.slice(1)}`, typeRef.name === type]), + ) as FieldViewModel["isPrimitive"]; + } + + private _collectNestedComplex(schema: TypeSchema | NestedType, cache: ViewModelCache): ResolvedTypeViewModel[] { + const nested: ResolvedTypeViewModel[] = []; + if ("nested" in schema && schema.nested) { + schema.nested + .map((nested) => this._createForNestedType(nested, cache, schema)) + .forEach((n) => { + nested.push(n); + }); + } + return nested; + } + private _collectNestedEnums(fields: [string, Field][]): EnumViewModel[] { + const nestedEnumValues: Record> = {}; + fields.forEach(([fieldName, fieldSchema]) => { + if ("enum" in fieldSchema && fieldSchema.enum) { + const name = ("binding" in fieldSchema && fieldSchema.binding?.name) ?? fieldName; + if (typeof name === "string") { + nestedEnumValues[name] = nestedEnumValues[name] ?? new Set(); + fieldSchema.enum?.forEach(nestedEnumValues[name].add.bind(nestedEnumValues[name])); + } + } + }); + return Object.entries(nestedEnumValues).map(([name, values]) => ({ + name: name, + saveName: this.nameGenerator.generateEnumType(name), + values: Array.from(values).map((value) => ({ + name: value, + saveName: this.nameGenerator.generateEnumValue(value), + })), + })); + } + + private _createForRoot(): Pick, "resources" | "complexTypes"> { + return this.arrayMixinProvider.apply({ + complexTypes: this.tsIndex + .collectComplexTypes() + .map((e) => e.identifier) + .map((typeRef: Identifier) => ({ + name: typeRef.name, + saveName: this.nameGenerator.generateType(typeRef), + })), + resources: this.tsIndex + .collectResources() + .map((e) => e.identifier) + .map((typeRef: Identifier) => ({ + name: typeRef.name, + saveName: this.nameGenerator.generateType(typeRef), + })), + }); + } +} diff --git a/src/api/mustache/types.ts b/src/api/mustache/types.ts new file mode 100644 index 000000000..8342eac6c --- /dev/null +++ b/src/api/mustache/types.ts @@ -0,0 +1,163 @@ +import type { IsPrefixed } from "@root/utils/types"; +import type { Field, NestedType, TypeSchema } from "@typeschema/types"; + +export type DebugMixin = { + debug: string; +}; + +export type EnumValueViewModel = { + name: string; + saveName: string; +}; + +export type EnumViewModel = NamedViewModel & { + values: EnumValueViewModel[]; +}; + +export type FieldViewModel = { + owner: NamedViewModel; + + schema: Field; + name: string; + saveName: string; + + typeName: string; + + isSizeConstrained: boolean; + min?: number; + max?: number; + + isArray: boolean; + isRequired: boolean; + isEnum: boolean; + + isPrimitive: Record, boolean> | false; + isComplexType: Record, boolean> | false; + isResource: Record, boolean> | false; + + isCode: boolean; + isIdentifier: boolean; + isReference: boolean; +}; + +export type FilterType = { + whitelist?: (string | RegExp)[]; + blacklist?: (string | RegExp)[]; +}; + +export type HookType = { + cmd: string; + args?: string[]; +}; + +export type LambdaMixin = { + lambda: { + saveEnumValueName: () => (text: string, render: (input: string) => string) => string; + saveFieldName: () => (text: string, render: (input: string) => string) => string; + saveTypeName: () => (text: string, render: (input: string) => string) => string; + + camelCase: () => (text: string, render: (input: string) => string) => string; + snakeCase: () => (text: string, render: (input: string) => string) => string; + pascalCase: () => (text: string, render: (input: string) => string) => string; + kebabCase: () => (text: string, render: (input: string) => string) => string; + lowerCase: () => (text: string, render: (input: string) => string) => string; + upperCase: () => (text: string, render: (input: string) => string) => string; + }; +}; + +export type ListElementInformationMixin = { + "-index": number; + "-length": number; + "-last": boolean; + "-first": boolean; +}; + +export type NamedViewModel = { + name: string; + saveName: string; +}; + +export const PRIMITIVE_TYPES = [ + "boolean", + "instant", + "time", + "date", + "dateTime", + + "decimal", + "integer", + "unsignedInt", + "positiveInt", + "integer64", + "base64Binary", + + "uri", + "url", + "canonical", + "oid", + "uuid", + + "string", + "code", + "markdown", + "id", + "xhtml", +] as const; + +export type PrimitiveType = (typeof PRIMITIVE_TYPES)[number]; + +export type Rendering = { + source: string; + fileNameFormat: string; + path: string; + filter?: FilterType; + properties?: Record; +}; + +export type ResolvedTypeViewModel = TypeViewModel & { + allFields: FieldViewModel[]; + inheritedFields: FieldViewModel[]; + parents: TypeViewModel[]; + children: TypeViewModel[]; + + hasChildren: boolean; + hasParents: boolean; + hasInheritedFields: boolean; +}; + +export type RootViewModel = T & { + resources: { name: string; saveName: string }[]; + complexTypes: { name: string; saveName: string }[]; +}; + +export type TypeViewModel = NamedViewModel & { + schema: TypeSchema | NestedType; + fields: FieldViewModel[]; + + dependencies: { + resources: NamedViewModel[]; + complexTypes: NamedViewModel[]; + }; + + hasFields: boolean; + hasNestedComplexTypes: boolean; + hasNestedEnums: boolean; + + isNested: boolean; + isComplexType: Record, boolean> | false; + isResource: Record, boolean> | false; + + nestedComplexTypes: ResolvedTypeViewModel[]; + nestedEnums: EnumViewModel[]; +}; + +export type View = LambdaMixin & { + model: T; + meta: { + timestamp: string; + generator: string; + }; + properties: Record; +}; + +export type ViewModel = Record; diff --git a/src/api/writer-generator/csharp/csharp.ts b/src/api/writer-generator/csharp/csharp.ts index b4739139e..71669c3ba 100644 --- a/src/api/writer-generator/csharp/csharp.ts +++ b/src/api/writer-generator/csharp/csharp.ts @@ -1,9 +1,9 @@ import fs from "node:fs"; import Path from "node:path"; import { fileURLToPath } from "node:url"; -import type { PartialBy } from "@root/api/builder.js"; import { pascalCase, uppercaseFirstLetter, uppercaseFirstLetterOfEach } from "@root/api/writer-generator/utils.ts"; import { Writer, type WriterOptions } from "@root/api/writer-generator/writer.ts"; +import type { PartialBy } from "@root/utils/types.ts"; import type { Field, Identifier, RegularField } from "@typeschema/types"; import { type ChoiceFieldInstance, isChoiceDeclarationField, type RegularTypeSchema } from "@typeschema/types.ts"; import type { TypeSchemaIndex } from "@typeschema/utils.ts"; diff --git a/src/api/writer-generator/mustache.ts b/src/api/writer-generator/mustache.ts new file mode 100644 index 000000000..cf270e680 --- /dev/null +++ b/src/api/writer-generator/mustache.ts @@ -0,0 +1,259 @@ +import { spawn } from "node:child_process"; +import * as fs from "node:fs"; +import * as Path from "node:path"; +import * as util from "node:util"; +import { DebugMixinProvider } from "@mustache/generator/DebugMixinProvider"; +import { LambdaMixinProvider } from "@mustache/generator/LambdaMixinProvider"; +import { + type DistinctNameConfigurationType, + NameGenerator, + type NameTransformation, +} from "@mustache/generator/NameGenerator"; +import { TemplateFileCache } from "@mustache/generator/TemplateFileCache"; +import type { ViewModelCache } from "@mustache/generator/ViewModelFactory"; +import { ViewModelFactory } from "@mustache/generator/ViewModelFactory"; +import type { + HookType, + NamedViewModel, + PrimitiveType, + Rendering, + TypeViewModel, + View, + ViewModel, +} from "@mustache/types"; +import type { TypeSchemaIndex } from "@root/typeschema/utils"; +import type { CodegenLogger } from "@root/utils/codegen-logger"; +import { default as Mustache } from "mustache"; +import { FileSystemWriter, type FileSystemWriterOptions } from "./writer"; + +export type FileBasedMustacheGeneratorOptions = { + debug: "OFF" | "FORMATTED" | "COMPACT"; + meta: { + timestamp?: string; + generator?: string; + }; + renderings: { + utility: Rendering[]; + resource: Rendering[]; + complexType: Rendering[]; + }; + keywords: string[]; + primitiveTypeMap: Partial>; + nameTransformations: DistinctNameConfigurationType; + unsaveCharacterPattern: string | RegExp; + shouldRunHooks: boolean; + hooks: { + afterGenerate?: HookType[]; + }; +}; + +export type MustacheGeneratorOptions = FileSystemWriterOptions & + FileBasedMustacheGeneratorOptions & { + sources: { + templateSource: string; + staticSource: string; + }; + }; + +export function loadMustacheGeneratorConfig( + templatePath: string, + logger?: CodegenLogger, +): Partial { + const filePath = Path.resolve(templatePath, "config.json"); + try { + const raw = fs.readFileSync(filePath, "utf-8"); + const parsed = JSON.parse(raw); + if (parsed && typeof parsed === "object") { + return parsed as Partial; + } + } catch (_e) {} + logger?.warn(`Failed to load JSON file with mustache generator config: ${filePath}`); + return {}; +} + +export const createGenerator = ( + templatePath: string, + apiOpts: FileSystemWriterOptions & Partial, +): MustacheGenerator => { + const defaultFileOpts: FileBasedMustacheGeneratorOptions = { + debug: "OFF", + hooks: {}, + meta: {}, + keywords: [], + unsaveCharacterPattern: /[^a-zA-Z0-9]/g, + nameTransformations: { + common: [], + enumValue: [], + type: [], + field: [], + }, + renderings: { + utility: [], + resource: [], + complexType: [], + }, + shouldRunHooks: true, + primitiveTypeMap: {}, + }; + const actualFileOpts = loadMustacheGeneratorConfig(templatePath); + + const mustacheOptions: MustacheGeneratorOptions = { + ...defaultFileOpts, + ...apiOpts, + ...actualFileOpts, + sources: { + staticSource: Path.resolve(templatePath, "static"), + templateSource: Path.resolve(templatePath, "templates"), + }, + }; + return new MustacheGenerator(mustacheOptions); +}; + +function runCommand(cmd: string, args: string[] = [], options = {}) { + return new Promise((resolve, reject) => { + const child = spawn(cmd, args, { + stdio: "inherit", + ...options, + }); + child.on("error", reject); + child.on("close", (code) => { + if (code === 0) resolve(code); + else reject(new Error(`Prozess beendet mit Fehlercode ${code}`)); + }); + }); +} + +export class MustacheGenerator extends FileSystemWriter { + private readonly templateFileCache: TemplateFileCache; + private readonly nameGenerator: NameGenerator; + private readonly lambdaMixinProvider: LambdaMixinProvider; + private readonly debugMixinProvider?: DebugMixinProvider; + + constructor(opts: MustacheGeneratorOptions) { + super(opts); + this.nameGenerator = new NameGenerator( + new Set(opts.keywords), + opts.primitiveTypeMap, + opts.nameTransformations, + opts.unsaveCharacterPattern, + ); + this.templateFileCache = new TemplateFileCache(opts.sources.templateSource); + this.lambdaMixinProvider = new LambdaMixinProvider(this.nameGenerator); + this.debugMixinProvider = opts.debug !== "OFF" ? new DebugMixinProvider(opts.debug) : undefined; + } + + override async generate(tsIndex: TypeSchemaIndex) { + const modelFactory = new ViewModelFactory(tsIndex, this.nameGenerator, () => true); + const cache: ViewModelCache = { + resourcesByUri: {}, + complexTypesByUri: {}, + }; + tsIndex + .collectComplexTypes() + .map((i) => i.identifier) + .sort((a, b) => a.url.localeCompare(b.url)) + .map((typeRef) => modelFactory.createComplexType(typeRef, cache)) + .forEach(this._renderComplexType.bind(this)); + + tsIndex + .collectResources() + .map((i) => i.identifier) + .sort((a, b) => a.url.localeCompare(b.url)) + .map((typeRef) => modelFactory.createResource(typeRef, cache)) + .forEach(this._renderResource.bind(this)); + + this._renderUtility(modelFactory.createUtility()); + this.copyStaticFiles(); + + if (this.opts.shouldRunHooks) { + await this._runHooks(this.opts.hooks.afterGenerate); + } + return; + } + + copyStaticFiles() { + const staticDir = Path.resolve(this.opts.sources.staticSource); + if (!staticDir) { + throw new Error("staticDir must be set in subclass."); + } + fs.cpSync(staticDir, this.opts.outputDir, { recursive: true }); + } + + private async _runHooks(hooks?: HookType[]) { + for (const hook of hooks ?? []) { + console.info(`Running hook (${this.opts.outputDir}): ${hook.cmd} ${hook.args?.join(" ")}`); + await runCommand(hook.cmd, hook.args ?? [], { + cwd: this.opts.outputDir, + }); + console.info(`Completed hook: ${hook.cmd} ${hook.args?.join(" ")}`); + } + } + + private _checkRenderingFilter(model: TypeViewModel, rendering: Rendering): boolean { + if (!rendering.filter?.whitelist?.length && !rendering.filter?.blacklist?.length) { + return true; + } + if ((rendering.filter?.blacklist ?? []).find((v) => model.name.match(v))) { + return false; + } + if ((rendering.filter?.whitelist ?? []).find((v) => model.name.match(v))) { + return true; + } + return !rendering.filter.whitelist?.length; + } + + private _renderUtility(model: ViewModel) { + this.opts.renderings.utility.forEach((rendering) => { + this.cd(rendering.path, () => { + this.cat(rendering.fileNameFormat, () => { + this.write(this._render(model, rendering)); + }); + }); + }); + } + + private _renderResource(model: TypeViewModel) { + this.opts.renderings.resource + .filter((rendering) => this._checkRenderingFilter(model, rendering)) + .forEach((rendering) => { + this.cd(rendering.path, () => { + this.cat(this._calculateFilename(model, rendering), () => { + this.write(this._render(model, rendering)); + }); + }); + }); + } + + private _renderComplexType(model: TypeViewModel) { + this.opts.renderings.complexType + .filter((rendering) => this._checkRenderingFilter(model, rendering)) + .forEach((rendering) => { + this.cd(rendering.path, () => { + this.cat(this._calculateFilename(model, rendering), () => { + this.write(this._render(model, rendering)); + }); + }); + }); + } + + private _calculateFilename(model: NamedViewModel, rendering: Rendering): string { + return util.format(rendering.fileNameFormat, model.saveName); + } + + private _render(model: T, rendering: Rendering): string { + let view: View = this.lambdaMixinProvider.apply({ + meta: { + timestamp: this.opts.meta.timestamp ?? new Date().toISOString(), + generator: this.opts.meta.generator ?? "@atomic-ehr/codegen mustache generator", + }, + model: model, + properties: rendering.properties ?? {}, + }); + if (this.debugMixinProvider) { + view = this.debugMixinProvider.apply(view); + } + return Mustache.render(this.templateFileCache.read(rendering), view, (partialName: string) => + this.templateFileCache.readTemplate(partialName), + ); + } +} diff --git a/src/typeschema/types.ts b/src/typeschema/types.ts index 8d9b6900e..9c7445471 100644 --- a/src/typeschema/types.ts +++ b/src/typeschema/types.ts @@ -94,18 +94,26 @@ export type Identifier = | ProfileIdentifier | LogicalIdentifier; -export const isPrimitiveIdentifier = (id: Identifier | undefined): id is PrimitiveIdentifier => { - return id?.kind === "primitive-type"; +export const isResourceIdentifier = (id: Identifier | undefined): id is ResourceIdentifier => { + return id?.kind === "resource"; }; -export const isNestedIdentifier = (id: Identifier | undefined): id is NestedIdentifier => { - return id?.kind === "nested"; +export const isLogicalIdentifier = (id: Identifier | undefined): id is LogicalIdentifier => { + return id?.kind === "logical"; }; export const isComplexTypeIdentifier = (id: Identifier | undefined): id is ComplexTypeIdentifier => { return id?.kind === "complex-type"; }; +export const isPrimitiveIdentifier = (id: Identifier | undefined): id is PrimitiveIdentifier => { + return id?.kind === "primitive-type"; +}; + +export const isNestedIdentifier = (id: Identifier | undefined): id is NestedIdentifier => { + return id?.kind === "nested"; +}; + export const isProfileIdentifier = (id: Identifier | undefined): id is ProfileIdentifier => { return id?.kind === "profile"; }; diff --git a/src/typeschema/utils.ts b/src/typeschema/utils.ts index 8e4b21609..842710795 100644 --- a/src/typeschema/utils.ts +++ b/src/typeschema/utils.ts @@ -104,7 +104,9 @@ interface TypeRelation { } const resourceRelatives = (schemas: TypeSchema[]): TypeRelation[] => { - const regularSchemas = schemas.filter((e) => isResourceTypeSchema(e) || isLogicalTypeSchema(e)); + const regularSchemas = schemas.filter( + (e) => isResourceTypeSchema(e) || isLogicalTypeSchema(e) || isComplexTypeTypeSchema(e), + ); const directPairs: TypeRelation[] = []; for (const schema of regularSchemas) { diff --git a/src/utils/types.ts b/src/utils/types.ts new file mode 100644 index 000000000..d13500452 --- /dev/null +++ b/src/utils/types.ts @@ -0,0 +1,5 @@ +export type CapitalizeFirst = S extends `${infer F}${infer R}` ? `${Uppercase}${R}` : S; + +export type IsPrefixed = `is${CapitalizeFirst}`; + +export type PartialBy = Omit & Partial>; diff --git a/test/api/__snapshots__/mustache.test.ts.snap b/test/api/__snapshots__/mustache.test.ts.snap new file mode 100644 index 000000000..b39de5251 --- /dev/null +++ b/test/api/__snapshots__/mustache.test.ts.snap @@ -0,0 +1,1418 @@ +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots + +exports[`Mustache Template Based Generation Patient resource 1`] = ` +"package de.solutio.fhir.models.resources; + +import static java.util.Objects.requireNonNull; + +import jakarta.annotation.Generated; +import jakarta.annotation.Nullable; +import jakarta.annotation.Generated; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import jakarta.validation.Valid; + +import de.solutio.fhir.models.Primitive; +import de.solutio.fhir.models.ResourceName; +import de.solutio.fhir.models.Utils; + + import de.solutio.fhir.models.complex_types.ReferenceDTO; +import de.solutio.fhir.models.complex_types.ElementDTO; + + import de.solutio.fhir.models.complex_types.AddressDTO; + import de.solutio.fhir.models.complex_types.AttachmentDTO; + import de.solutio.fhir.models.complex_types.BackboneElementDTO; + import de.solutio.fhir.models.complex_types.CodeableConceptDTO; + import de.solutio.fhir.models.complex_types.ContactPointDTO; + import de.solutio.fhir.models.complex_types.HumanNameDTO; + import de.solutio.fhir.models.complex_types.IdentifierDTO; + import de.solutio.fhir.models.complex_types.PeriodDTO; + import de.solutio.fhir.models.complex_types.ReferenceDTO; + + import de.solutio.fhir.models.resources.DomainResourceDTO; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; + +import java.net.URI; +import java.math.BigDecimal; +import java.time.OffsetDateTime; +import java.time.LocalTime; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.List; +import java.util.UUID; +import java.util.Collections; +import java.util.Objects; +import java.util.Optional; +import java.util.HashMap; +import java.util.Map; +import java.util.Arrays; +import java.util.ArrayList; +import java.util.stream.IntStream; + +@Generated(value="@atomic-ehr/codegen mustache generator", date="2025-12-24T00:00:00.000Z") +@JsonTypeName("Patient") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE, creatorVisibility = JsonAutoDetect.Visibility.NONE, scalarConstructorVisibility = JsonAutoDetect.Visibility.NONE) +public class PatientDTO extends DomainResourceDTO{ + public static final ResourceName NAME = ResourceName.PATIENT_DTO; + + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE, creatorVisibility = JsonAutoDetect.Visibility.NONE, scalarConstructorVisibility = JsonAutoDetect.Visibility.NONE) + public static class PatientCommunicationDTO extends BackboneElementDTO{ + + + + private final @NotNull + + + + @JsonProperty("language") @Valid CodeableConceptDTO language; + private final @Nullable + + + + @JsonProperty("preferred") Boolean preferred; + private final @Nullable @JsonProperty("_preferred") ElementDTO _preferred; + + protected PatientCommunicationDTO(){ + this.language = null; + this.preferred = null; + this._preferred = null; + } + + + public CodeableConceptDTO language() { + return this.language; + } + public Optional preferred() { + return Optional.ofNullable(this.preferred); + } + public Optional _preferred() { + return Optional.ofNullable(this._preferred); + } + + public static PatientCommunicationDTOBuilder patientCommunicationDtoBuilder() { + return new PatientCommunicationDTOBuilder(); + } + + + + protected static abstract class AbstractPatientCommunicationDTOBuilder> extends AbstractBackboneElementDTOBuilder{ + + private @Nullable CodeableConceptDTO language = null; + private @Nullable Primitive preferred = null; + + protected AbstractPatientCommunicationDTOBuilder() { + } + + + public final B language(@Nullable CodeableConceptDTO language) { + this.language = language; + return this.self(); + } + public final B preferred(@Nullable Boolean preferred) { + return this.preferred(preferred, null); + } + public final B _preferred(@Nullable ElementDTO preferredElement) { + requireNonNull(this.preferred, "preferred must not be null to set preferredElement"); + return this.preferred(this.preferred.value(), preferredElement); + } + public final B preferred(@Nullable Boolean preferred, @Nullable ElementDTO preferredElement) { + this.preferred = preferred != null ? Primitive.nonNullable(preferred, preferredElement) : null; + return this.self(); + } + + protected abstract B self(); + } public static final class PatientCommunicationDTOBuilder extends AbstractPatientCommunicationDTOBuilder{ + private PatientCommunicationDTOBuilder(){ + } + protected PatientCommunicationDTOBuilder self(){ + return this; + } + public PatientCommunicationDTO build(){ + return new PatientCommunicationDTO(this); + } + } + private PatientCommunicationDTO(PatientCommunicationDTOBuilder builder) { + this((AbstractPatientCommunicationDTOBuilder)requireNonNull(builder)); + } + + protected PatientCommunicationDTO(AbstractPatientCommunicationDTOBuilder builder) { + super(requireNonNull(builder)); + requireNonNull(builder.language, "missing 'language'"); + + this.language = builder.language; + this.preferred = builder.preferred != null ? requireNonNull(builder.preferred.value()) : null; + this._preferred = builder.preferred != null ? builder.preferred.element() : null; + } + protected > B applyToBuilder(B builder) { + requireNonNull(builder); + super.applyToBuilder(builder); + builder + .language(this.language) + .preferred(this.preferred,this._preferred) + ; + return builder; + } + public final PatientCommunicationDTOBuilder toBuilder() { + return this.applyToBuilder(PatientCommunicationDTO.patientCommunicationDtoBuilder()); + } + + public int hashCode() { + return Objects.hash(super.hashCode(),this.language, this.preferred); + } + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (obj == null) { + return false; + }else if(!super.equals(obj)) { + return false; + } else if (!(obj instanceof PatientCommunicationDTO)) { + return false; + } + final PatientCommunicationDTO other = (PatientCommunicationDTO) obj; + return Utils.equals(this.language, other.language) && Utils.equals(this.preferred, other.preferred); + } + protected Utils.ToStringHelper toStringHelper(){ + return super.toStringHelper() + .add("language", this.language) + .add("preferred", this.preferred) + .add("_preferred", this._preferred) + ; + } + + } @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE, creatorVisibility = JsonAutoDetect.Visibility.NONE, scalarConstructorVisibility = JsonAutoDetect.Visibility.NONE) + public static class PatientContactDTO extends BackboneElementDTO{ + + + public enum AdministrativeGenderDTO { + MALE("male"), + FEMALE("female"), + OTHER("other"), + UNKNOWN("unknown") + ; + + private static final Map BY_VALUE; + static { + final Map builder = new HashMap<>(); + for (AdministrativeGenderDTO c: AdministrativeGenderDTO.values()) { + builder.put(c.value, c); + } + BY_VALUE = Collections.unmodifiableMap(builder); + } + @JsonCreator + public static AdministrativeGenderDTO fromValue(String value) { + requireNonNull(value); + if(AdministrativeGenderDTO.BY_VALUE.containsKey(value)) { + return BY_VALUE.get(value); + } + throw new IllegalArgumentException("Unexpected value '" + value + "'"); + } + + private final String value; + AdministrativeGenderDTO(String value) { + this.value = requireNonNull(value); + } + + @JsonValue + public String getValue() { + return value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + } + private final @Nullable + + + + @JsonProperty("address") @Valid AddressDTO address; + private final @Nullable + + + + @JsonProperty("gender") AdministrativeGenderDTO gender; + private final @Nullable @JsonProperty("_gender") ElementDTO _gender; + private final @Nullable + + + + @JsonProperty("name") @Valid HumanNameDTO name; + private final @Nullable + + + + @JsonProperty("organization") @Valid ReferenceDTO organization; + private final @Nullable + + + + @JsonProperty("period") @Valid PeriodDTO period; + private final @Nullable + + + + @JsonProperty("relationship") List<@Valid CodeableConceptDTO> relationship; + private final @Nullable + + + + @JsonProperty("telecom") List<@Valid ContactPointDTO> telecom; + + protected PatientContactDTO(){ + this.address = null; + this.gender = null; + this._gender = null; + this.name = null; + this.organization = null; + this.period = null; + this.relationship = null; + this.telecom = null; + } + + + public Optional address() { + return Optional.ofNullable(this.address); + } + public Optional gender() { + return Optional.ofNullable(this.gender); + } + public Optional _gender() { + return Optional.ofNullable(this._gender); + } + public Optional name() { + return Optional.ofNullable(this.name); + } + public Optional organization() { + return Optional.ofNullable(this.organization); + } + public Optional period() { + return Optional.ofNullable(this.period); + } + public Optional> relationship() { + return Optional.ofNullable(this.relationship); + } + public Optional> telecom() { + return Optional.ofNullable(this.telecom); + } + + public static PatientContactDTOBuilder patientContactDtoBuilder() { + return new PatientContactDTOBuilder(); + } + + + + protected static abstract class AbstractPatientContactDTOBuilder> extends AbstractBackboneElementDTOBuilder{ + + private @Nullable AddressDTO address = null; + private @Nullable Primitive gender = null; + private @Nullable HumanNameDTO name = null; + private @Nullable ReferenceDTO organization = null; + private @Nullable PeriodDTO period = null; + private @Nullable List relationship = null; + private @Nullable List telecom = null; + + protected AbstractPatientContactDTOBuilder() { + } + + + public final B address(@Nullable AddressDTO address) { + this.address = address; + return this.self(); + } + public final B gender(@Nullable AdministrativeGenderDTO gender) { + return this.gender(gender, null); + } + public final B _gender(@Nullable ElementDTO genderElement) { + requireNonNull(this.gender, "gender must not be null to set genderElement"); + return this.gender(this.gender.value(), genderElement); + } + public final B gender(@Nullable AdministrativeGenderDTO gender, @Nullable ElementDTO genderElement) { + this.gender = gender != null ? Primitive.nonNullable(gender, genderElement) : null; + return this.self(); + } + public final B name(@Nullable HumanNameDTO name) { + this.name = name; + return this.self(); + } + public final B organization(@Nullable ReferenceDTO organization) { + this.organization = organization; + return this.self(); + } + public final B period(@Nullable PeriodDTO period) { + this.period = period; + return this.self(); + } + private List relationship(){ + if(this.relationship == null) { + this.relationship = new ArrayList<>(); + } + return this.relationship; + } + private void clearRelationship(){ + if(this.relationship != null) { + this.relationship.clear(); + } + } + public final B relationship(@Nullable CodeableConceptDTO relationship) { + return this.relationship(relationship != null ? List.of(relationship) : null); + } + public final B relationship(@Nullable List relationship) { + this.clearRelationship(); + return this.addRelationship(relationship); + } + public final B addRelationship(@Nullable CodeableConceptDTO relationship) { + return this.addRelationship(relationship != null ? List.of(relationship) : null); + } + public final B addRelationship(@Nullable List relationship) { + if (relationship != null) { + this.relationship().addAll(relationship); + } + return this.self(); + } + private List telecom(){ + if(this.telecom == null) { + this.telecom = new ArrayList<>(); + } + return this.telecom; + } + private void clearTelecom(){ + if(this.telecom != null) { + this.telecom.clear(); + } + } + public final B telecom(@Nullable ContactPointDTO telecom) { + return this.telecom(telecom != null ? List.of(telecom) : null); + } + public final B telecom(@Nullable List telecom) { + this.clearTelecom(); + return this.addTelecom(telecom); + } + public final B addTelecom(@Nullable ContactPointDTO telecom) { + return this.addTelecom(telecom != null ? List.of(telecom) : null); + } + public final B addTelecom(@Nullable List telecom) { + if (telecom != null) { + this.telecom().addAll(telecom); + } + return this.self(); + } + + protected abstract B self(); + } public static final class PatientContactDTOBuilder extends AbstractPatientContactDTOBuilder{ + private PatientContactDTOBuilder(){ + } + protected PatientContactDTOBuilder self(){ + return this; + } + public PatientContactDTO build(){ + return new PatientContactDTO(this); + } + } + private PatientContactDTO(PatientContactDTOBuilder builder) { + this((AbstractPatientContactDTOBuilder)requireNonNull(builder)); + } + + protected PatientContactDTO(AbstractPatientContactDTOBuilder builder) { + super(requireNonNull(builder)); + + + + + + + + this.address = builder.address; + this.gender = builder.gender != null ? requireNonNull(builder.gender.value()) : null; + this._gender = builder.gender != null ? builder.gender.element() : null; + this.name = builder.name; + this.organization = builder.organization; + this.period = builder.period; + this.relationship = builder.relationship != null ? List.copyOf(builder.relationship) : null; + this.telecom = builder.telecom != null ? List.copyOf(builder.telecom) : null; + } + protected > B applyToBuilder(B builder) { + requireNonNull(builder); + super.applyToBuilder(builder); + builder + .address(this.address) + .gender(this.gender,this._gender) + .name(this.name) + .organization(this.organization) + .period(this.period) + .relationship(this.relationship) + .telecom(this.telecom) + ; + return builder; + } + public final PatientContactDTOBuilder toBuilder() { + return this.applyToBuilder(PatientContactDTO.patientContactDtoBuilder()); + } + + public int hashCode() { + return Objects.hash(super.hashCode(),this.address, this.gender, this.name, this.organization, this.period, this.relationship, this.telecom); + } + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (obj == null) { + return false; + }else if(!super.equals(obj)) { + return false; + } else if (!(obj instanceof PatientContactDTO)) { + return false; + } + final PatientContactDTO other = (PatientContactDTO) obj; + return Utils.equals(this.address, other.address) && Utils.equals(this.gender, other.gender) && Utils.equals(this.name, other.name) && Utils.equals(this.organization, other.organization) && Utils.equals(this.period, other.period) && Utils.equals(this.relationship, other.relationship) && Utils.equals(this.telecom, other.telecom); + } + protected Utils.ToStringHelper toStringHelper(){ + return super.toStringHelper() + .add("address", this.address) + .add("gender", this.gender) + .add("_gender", this._gender) + .add("name", this.name) + .add("organization", this.organization) + .add("period", this.period) + .add("relationship", this.relationship) + .add("telecom", this.telecom) + ; + } + + } @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE, creatorVisibility = JsonAutoDetect.Visibility.NONE, scalarConstructorVisibility = JsonAutoDetect.Visibility.NONE) + public static class PatientLinkDTO extends BackboneElementDTO{ + + + public enum LinkTypeDTO { + REPLACED_BY("replaced-by"), + REPLACES("replaces"), + REFER("refer"), + SEEALSO("seealso") + ; + + private static final Map BY_VALUE; + static { + final Map builder = new HashMap<>(); + for (LinkTypeDTO c: LinkTypeDTO.values()) { + builder.put(c.value, c); + } + BY_VALUE = Collections.unmodifiableMap(builder); + } + @JsonCreator + public static LinkTypeDTO fromValue(String value) { + requireNonNull(value); + if(LinkTypeDTO.BY_VALUE.containsKey(value)) { + return BY_VALUE.get(value); + } + throw new IllegalArgumentException("Unexpected value '" + value + "'"); + } + + private final String value; + LinkTypeDTO(String value) { + this.value = requireNonNull(value); + } + + @JsonValue + public String getValue() { + return value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + } + private final @NotNull + + + + @JsonProperty("other") @Valid ReferenceDTO other; + private final @NotNull + + + + @JsonProperty("type") LinkTypeDTO type; + private final @Nullable @JsonProperty("_type") ElementDTO _type; + + protected PatientLinkDTO(){ + this.other = null; + this.type = null; + this._type = null; + } + + + public ReferenceDTO other() { + return this.other; + } + public LinkTypeDTO type() { + return this.type; + } + public Optional _type() { + return Optional.ofNullable(this._type); + } + + public static PatientLinkDTOBuilder patientLinkDtoBuilder() { + return new PatientLinkDTOBuilder(); + } + + + + protected static abstract class AbstractPatientLinkDTOBuilder> extends AbstractBackboneElementDTOBuilder{ + + private @Nullable ReferenceDTO other = null; + private @Nullable Primitive type = null; + + protected AbstractPatientLinkDTOBuilder() { + } + + + public final B other(@Nullable ReferenceDTO other) { + this.other = other; + return this.self(); + } + public final B type(@Nullable LinkTypeDTO type) { + return this.type(type, null); + } + public final B _type(@Nullable ElementDTO typeElement) { + requireNonNull(this.type, "type must not be null to set typeElement"); + return this.type(this.type.value(), typeElement); + } + public final B type(@Nullable LinkTypeDTO type, @Nullable ElementDTO typeElement) { + this.type = type != null ? Primitive.nonNullable(type, typeElement) : null; + return this.self(); + } + + protected abstract B self(); + } public static final class PatientLinkDTOBuilder extends AbstractPatientLinkDTOBuilder{ + private PatientLinkDTOBuilder(){ + } + protected PatientLinkDTOBuilder self(){ + return this; + } + public PatientLinkDTO build(){ + return new PatientLinkDTO(this); + } + } + private PatientLinkDTO(PatientLinkDTOBuilder builder) { + this((AbstractPatientLinkDTOBuilder)requireNonNull(builder)); + } + + protected PatientLinkDTO(AbstractPatientLinkDTOBuilder builder) { + super(requireNonNull(builder)); + requireNonNull(builder.other, "missing 'other'"); + requireNonNull(builder.type, "missing 'type'"); + this.other = builder.other; + this.type = requireNonNull(builder.type.value()); + this._type = builder.type.element(); + } + protected > B applyToBuilder(B builder) { + requireNonNull(builder); + super.applyToBuilder(builder); + builder + .other(this.other) + .type(this.type,this._type) + ; + return builder; + } + public final PatientLinkDTOBuilder toBuilder() { + return this.applyToBuilder(PatientLinkDTO.patientLinkDtoBuilder()); + } + + public int hashCode() { + return Objects.hash(super.hashCode(),this.other, this.type); + } + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (obj == null) { + return false; + }else if(!super.equals(obj)) { + return false; + } else if (!(obj instanceof PatientLinkDTO)) { + return false; + } + final PatientLinkDTO other = (PatientLinkDTO) obj; + return Utils.equals(this.other, other.other) && Utils.equals(this.type, other.type); + } + protected Utils.ToStringHelper toStringHelper(){ + return super.toStringHelper() + .add("other", this.other) + .add("type", this.type) + .add("_type", this._type) + ; + } + + } + public enum AdministrativeGenderDTO { + MALE("male"), + FEMALE("female"), + OTHER("other"), + UNKNOWN("unknown") + ; + + private static final Map BY_VALUE; + static { + final Map builder = new HashMap<>(); + for (AdministrativeGenderDTO c: AdministrativeGenderDTO.values()) { + builder.put(c.value, c); + } + BY_VALUE = Collections.unmodifiableMap(builder); + } + @JsonCreator + public static AdministrativeGenderDTO fromValue(String value) { + requireNonNull(value); + if(AdministrativeGenderDTO.BY_VALUE.containsKey(value)) { + return BY_VALUE.get(value); + } + throw new IllegalArgumentException("Unexpected value '" + value + "'"); + } + + private final String value; + AdministrativeGenderDTO(String value) { + this.value = requireNonNull(value); + } + + @JsonValue + public String getValue() { + return value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + } + private final @Nullable + + + + @JsonProperty("active") Boolean active; + private final @Nullable @JsonProperty("_active") ElementDTO _active; + private final @Nullable + + + + @JsonProperty("address") List<@Valid AddressDTO> address; + private final @Nullable + + + + @JsonProperty("birthDate") String birthDate; + private final @Nullable @JsonProperty("_birthDate") ElementDTO _birthDate; + private final @Nullable + + + + @JsonProperty("communication") List<@Valid PatientCommunicationDTO> communication; + private final @Nullable + + + + @JsonProperty("contact") List<@Valid PatientContactDTO> contact; + private final @Nullable + + + + @JsonProperty("deceasedBoolean") Boolean deceasedBoolean; + private final @Nullable @JsonProperty("_deceasedBoolean") ElementDTO _deceasedBoolean; + private final @Nullable + + + @JsonFormat(shape = JsonFormat.Shape.STRING) + @JsonProperty("deceasedDateTime") OffsetDateTime deceasedDateTime; + private final @Nullable @JsonProperty("_deceasedDateTime") ElementDTO _deceasedDateTime; + private final @Nullable + + + + @JsonProperty("gender") AdministrativeGenderDTO gender; + private final @Nullable @JsonProperty("_gender") ElementDTO _gender; + private final @Nullable + + + + @JsonProperty("generalPractitioner") List<@Valid ReferenceDTO> generalPractitioner; + private final @Nullable + + + + @JsonProperty("identifier") List<@Valid IdentifierDTO> identifier; + private final @Nullable + + + + @JsonProperty("link") List<@Valid PatientLinkDTO> link; + private final @Nullable + + + + @JsonProperty("managingOrganization") @Valid ReferenceDTO managingOrganization; + private final @Nullable + + + + @JsonProperty("maritalStatus") @Valid CodeableConceptDTO maritalStatus; + private final @Nullable + + + + @JsonProperty("multipleBirthBoolean") Boolean multipleBirthBoolean; + private final @Nullable @JsonProperty("_multipleBirthBoolean") ElementDTO _multipleBirthBoolean; + private final @Nullable + + + + @JsonProperty("multipleBirthInteger") Integer multipleBirthInteger; + private final @Nullable @JsonProperty("_multipleBirthInteger") ElementDTO _multipleBirthInteger; + private final @Nullable + + + + @JsonProperty("name") List<@Valid HumanNameDTO> name; + private final @Nullable + + + + @JsonProperty("photo") List<@Valid AttachmentDTO> photo; + private final @Nullable + + + + @JsonProperty("telecom") List<@Valid ContactPointDTO> telecom; + + protected PatientDTO(){ + this.active = null; + this._active = null; + this.address = null; + this.birthDate = null; + this._birthDate = null; + this.communication = null; + this.contact = null; + this.deceasedBoolean = null; + this._deceasedBoolean = null; + this.deceasedDateTime = null; + this._deceasedDateTime = null; + this.gender = null; + this._gender = null; + this.generalPractitioner = null; + this.identifier = null; + this.link = null; + this.managingOrganization = null; + this.maritalStatus = null; + this.multipleBirthBoolean = null; + this._multipleBirthBoolean = null; + this.multipleBirthInteger = null; + this._multipleBirthInteger = null; + this.name = null; + this.photo = null; + this.telecom = null; + } + public ResourceName resourceName() { + return PatientDTO.NAME; + } + public ReferenceDTO toReference() { + return ReferenceDTO.referenceDtoBuilder() + .reference(this.resourceName(), this.id().orElseThrow(() -> new IllegalStateException("missing id"))) + .build(); + } + + + public Optional active() { + return Optional.ofNullable(this.active); + } + public Optional _active() { + return Optional.ofNullable(this._active); + } + public Optional> address() { + return Optional.ofNullable(this.address); + } + public Optional birthDate() { + return Optional.ofNullable(this.birthDate); + } + public Optional _birthDate() { + return Optional.ofNullable(this._birthDate); + } + public Optional> communication() { + return Optional.ofNullable(this.communication); + } + public Optional> contact() { + return Optional.ofNullable(this.contact); + } + public Optional deceasedBoolean() { + return Optional.ofNullable(this.deceasedBoolean); + } + public Optional _deceasedBoolean() { + return Optional.ofNullable(this._deceasedBoolean); + } + public Optional deceasedDateTime() { + return Optional.ofNullable(this.deceasedDateTime); + } + public Optional _deceasedDateTime() { + return Optional.ofNullable(this._deceasedDateTime); + } + public Optional deceasedDateTimeAsLocalDateTime() { + return Optional.ofNullable(this.deceasedDateTime).map(odt->odt.atZoneSameInstant(ZoneId.systemDefault()).toLocalDateTime()); + } + public Optional gender() { + return Optional.ofNullable(this.gender); + } + public Optional _gender() { + return Optional.ofNullable(this._gender); + } + public Optional> generalPractitioner() { + return Optional.ofNullable(this.generalPractitioner); + } + public Optional> identifier() { + return Optional.ofNullable(this.identifier); + } + public Optional> link() { + return Optional.ofNullable(this.link); + } + public Optional managingOrganization() { + return Optional.ofNullable(this.managingOrganization); + } + public Optional maritalStatus() { + return Optional.ofNullable(this.maritalStatus); + } + public Optional multipleBirthBoolean() { + return Optional.ofNullable(this.multipleBirthBoolean); + } + public Optional _multipleBirthBoolean() { + return Optional.ofNullable(this._multipleBirthBoolean); + } + public Optional multipleBirthInteger() { + return Optional.ofNullable(this.multipleBirthInteger); + } + public Optional _multipleBirthInteger() { + return Optional.ofNullable(this._multipleBirthInteger); + } + public Optional> name() { + return Optional.ofNullable(this.name); + } + public Optional> photo() { + return Optional.ofNullable(this.photo); + } + public Optional> telecom() { + return Optional.ofNullable(this.telecom); + } + + public static PatientDTOBuilder patientDtoBuilder() { + return new PatientDTOBuilder(); + } + + + + protected static abstract class AbstractPatientDTOBuilder> extends AbstractDomainResourceDTOBuilder{ + + private @Nullable Primitive active = null; + private @Nullable List address = null; + private @Nullable Primitive birthDate = null; + private @Nullable List communication = null; + private @Nullable List contact = null; + private @Nullable Primitive deceasedBoolean = null; + private @Nullable Primitive deceasedDateTime = null; + private @Nullable Primitive gender = null; + private @Nullable List generalPractitioner = null; + private @Nullable List identifier = null; + private @Nullable List link = null; + private @Nullable ReferenceDTO managingOrganization = null; + private @Nullable CodeableConceptDTO maritalStatus = null; + private @Nullable Primitive multipleBirthBoolean = null; + private @Nullable Primitive multipleBirthInteger = null; + private @Nullable List name = null; + private @Nullable List photo = null; + private @Nullable List telecom = null; + + protected AbstractPatientDTOBuilder() { + } + + + public final B active(@Nullable Boolean active) { + return this.active(active, null); + } + public final B _active(@Nullable ElementDTO activeElement) { + requireNonNull(this.active, "active must not be null to set activeElement"); + return this.active(this.active.value(), activeElement); + } + public final B active(@Nullable Boolean active, @Nullable ElementDTO activeElement) { + this.active = active != null ? Primitive.nonNullable(active, activeElement) : null; + return this.self(); + } + private List address(){ + if(this.address == null) { + this.address = new ArrayList<>(); + } + return this.address; + } + private void clearAddress(){ + if(this.address != null) { + this.address.clear(); + } + } + public final B address(@Nullable AddressDTO address) { + return this.address(address != null ? List.of(address) : null); + } + public final B address(@Nullable List address) { + this.clearAddress(); + return this.addAddress(address); + } + public final B addAddress(@Nullable AddressDTO address) { + return this.addAddress(address != null ? List.of(address) : null); + } + public final B addAddress(@Nullable List address) { + if (address != null) { + this.address().addAll(address); + } + return this.self(); + } + public final B birthDate(@Nullable String birthDate) { + return this.birthDate(birthDate, null); + } + public final B _birthDate(@Nullable ElementDTO birthDateElement) { + requireNonNull(this.birthDate, "birthDate must not be null to set birthDateElement"); + return this.birthDate(this.birthDate.value(), birthDateElement); + } + public final B birthDate(@Nullable String birthDate, @Nullable ElementDTO birthDateElement) { + this.birthDate = birthDate != null ? Primitive.nonNullable(birthDate, birthDateElement) : null; + return this.self(); + } + private List communication(){ + if(this.communication == null) { + this.communication = new ArrayList<>(); + } + return this.communication; + } + private void clearCommunication(){ + if(this.communication != null) { + this.communication.clear(); + } + } + public final B communication(@Nullable PatientCommunicationDTO communication) { + return this.communication(communication != null ? List.of(communication) : null); + } + public final B communication(@Nullable List communication) { + this.clearCommunication(); + return this.addCommunication(communication); + } + public final B addCommunication(@Nullable PatientCommunicationDTO communication) { + return this.addCommunication(communication != null ? List.of(communication) : null); + } + public final B addCommunication(@Nullable List communication) { + if (communication != null) { + this.communication().addAll(communication); + } + return this.self(); + } + private List contact(){ + if(this.contact == null) { + this.contact = new ArrayList<>(); + } + return this.contact; + } + private void clearContact(){ + if(this.contact != null) { + this.contact.clear(); + } + } + public final B contact(@Nullable PatientContactDTO contact) { + return this.contact(contact != null ? List.of(contact) : null); + } + public final B contact(@Nullable List contact) { + this.clearContact(); + return this.addContact(contact); + } + public final B addContact(@Nullable PatientContactDTO contact) { + return this.addContact(contact != null ? List.of(contact) : null); + } + public final B addContact(@Nullable List contact) { + if (contact != null) { + this.contact().addAll(contact); + } + return this.self(); + } + public final B deceasedBoolean(@Nullable Boolean deceasedBoolean) { + return this.deceasedBoolean(deceasedBoolean, null); + } + public final B _deceasedBoolean(@Nullable ElementDTO deceasedBooleanElement) { + requireNonNull(this.deceasedBoolean, "deceasedBoolean must not be null to set deceasedBooleanElement"); + return this.deceasedBoolean(this.deceasedBoolean.value(), deceasedBooleanElement); + } + public final B deceasedBoolean(@Nullable Boolean deceasedBoolean, @Nullable ElementDTO deceasedBooleanElement) { + this.deceasedBoolean = deceasedBoolean != null ? Primitive.nonNullable(deceasedBoolean, deceasedBooleanElement) : null; + return this.self(); + } + public final B deceasedDateTime(@Nullable OffsetDateTime deceasedDateTime) { + return this.deceasedDateTime(deceasedDateTime, null); + } + public final B _deceasedDateTime(@Nullable ElementDTO deceasedDateTimeElement) { + requireNonNull(this.deceasedDateTime, "deceasedDateTime must not be null to set deceasedDateTimeElement"); + return this.deceasedDateTime(this.deceasedDateTime.value(), deceasedDateTimeElement); + } + public final B deceasedDateTime(@Nullable OffsetDateTime deceasedDateTime, @Nullable ElementDTO deceasedDateTimeElement) { + this.deceasedDateTime = deceasedDateTime != null ? Primitive.nonNullable(deceasedDateTime, deceasedDateTimeElement) : null; + return this.self(); + } + public final B deceasedDateTime(@Nullable LocalDateTime deceasedDateTime) { + return this.deceasedDateTime(deceasedDateTime, null); + } + public final B deceasedDateTime(@Nullable LocalDateTime deceasedDateTime, @Nullable ElementDTO deceasedDateTimeElement) { + return this.deceasedDateTime(deceasedDateTime != null ? deceasedDateTime.atZone(ZoneId.systemDefault()).toOffsetDateTime() : null, deceasedDateTimeElement); + } + public final B gender(@Nullable AdministrativeGenderDTO gender) { + return this.gender(gender, null); + } + public final B _gender(@Nullable ElementDTO genderElement) { + requireNonNull(this.gender, "gender must not be null to set genderElement"); + return this.gender(this.gender.value(), genderElement); + } + public final B gender(@Nullable AdministrativeGenderDTO gender, @Nullable ElementDTO genderElement) { + this.gender = gender != null ? Primitive.nonNullable(gender, genderElement) : null; + return this.self(); + } + private List generalPractitioner(){ + if(this.generalPractitioner == null) { + this.generalPractitioner = new ArrayList<>(); + } + return this.generalPractitioner; + } + private void clearGeneralPractitioner(){ + if(this.generalPractitioner != null) { + this.generalPractitioner.clear(); + } + } + public final B generalPractitioner(@Nullable ReferenceDTO generalPractitioner) { + return this.generalPractitioner(generalPractitioner != null ? List.of(generalPractitioner) : null); + } + public final B generalPractitioner(@Nullable List generalPractitioner) { + this.clearGeneralPractitioner(); + return this.addGeneralPractitioner(generalPractitioner); + } + public final B addGeneralPractitioner(@Nullable ReferenceDTO generalPractitioner) { + return this.addGeneralPractitioner(generalPractitioner != null ? List.of(generalPractitioner) : null); + } + public final B addGeneralPractitioner(@Nullable List generalPractitioner) { + if (generalPractitioner != null) { + this.generalPractitioner().addAll(generalPractitioner); + } + return this.self(); + } + private List identifier(){ + if(this.identifier == null) { + this.identifier = new ArrayList<>(); + } + return this.identifier; + } + private void clearIdentifier(){ + if(this.identifier != null) { + this.identifier.clear(); + } + } + public final B identifier(@Nullable IdentifierDTO identifier) { + return this.identifier(identifier != null ? List.of(identifier) : null); + } + public final B identifier(@Nullable List identifier) { + this.clearIdentifier(); + return this.addIdentifier(identifier); + } + public final B addIdentifier(@Nullable IdentifierDTO identifier) { + return this.addIdentifier(identifier != null ? List.of(identifier) : null); + } + public final B addIdentifier(@Nullable List identifier) { + if (identifier != null) { + this.identifier().addAll(identifier); + } + return this.self(); + } + private List link(){ + if(this.link == null) { + this.link = new ArrayList<>(); + } + return this.link; + } + private void clearLink(){ + if(this.link != null) { + this.link.clear(); + } + } + public final B link(@Nullable PatientLinkDTO link) { + return this.link(link != null ? List.of(link) : null); + } + public final B link(@Nullable List link) { + this.clearLink(); + return this.addLink(link); + } + public final B addLink(@Nullable PatientLinkDTO link) { + return this.addLink(link != null ? List.of(link) : null); + } + public final B addLink(@Nullable List link) { + if (link != null) { + this.link().addAll(link); + } + return this.self(); + } + public final B managingOrganization(@Nullable ReferenceDTO managingOrganization) { + this.managingOrganization = managingOrganization; + return this.self(); + } + public final B maritalStatus(@Nullable CodeableConceptDTO maritalStatus) { + this.maritalStatus = maritalStatus; + return this.self(); + } + public final B multipleBirthBoolean(@Nullable Boolean multipleBirthBoolean) { + return this.multipleBirthBoolean(multipleBirthBoolean, null); + } + public final B _multipleBirthBoolean(@Nullable ElementDTO multipleBirthBooleanElement) { + requireNonNull(this.multipleBirthBoolean, "multipleBirthBoolean must not be null to set multipleBirthBooleanElement"); + return this.multipleBirthBoolean(this.multipleBirthBoolean.value(), multipleBirthBooleanElement); + } + public final B multipleBirthBoolean(@Nullable Boolean multipleBirthBoolean, @Nullable ElementDTO multipleBirthBooleanElement) { + this.multipleBirthBoolean = multipleBirthBoolean != null ? Primitive.nonNullable(multipleBirthBoolean, multipleBirthBooleanElement) : null; + return this.self(); + } + public final B multipleBirthInteger(@Nullable Integer multipleBirthInteger) { + return this.multipleBirthInteger(multipleBirthInteger, null); + } + public final B _multipleBirthInteger(@Nullable ElementDTO multipleBirthIntegerElement) { + requireNonNull(this.multipleBirthInteger, "multipleBirthInteger must not be null to set multipleBirthIntegerElement"); + return this.multipleBirthInteger(this.multipleBirthInteger.value(), multipleBirthIntegerElement); + } + public final B multipleBirthInteger(@Nullable Integer multipleBirthInteger, @Nullable ElementDTO multipleBirthIntegerElement) { + this.multipleBirthInteger = multipleBirthInteger != null ? Primitive.nonNullable(multipleBirthInteger, multipleBirthIntegerElement) : null; + return this.self(); + } + private List name(){ + if(this.name == null) { + this.name = new ArrayList<>(); + } + return this.name; + } + private void clearName(){ + if(this.name != null) { + this.name.clear(); + } + } + public final B name(@Nullable HumanNameDTO name) { + return this.name(name != null ? List.of(name) : null); + } + public final B name(@Nullable List name) { + this.clearName(); + return this.addName(name); + } + public final B addName(@Nullable HumanNameDTO name) { + return this.addName(name != null ? List.of(name) : null); + } + public final B addName(@Nullable List name) { + if (name != null) { + this.name().addAll(name); + } + return this.self(); + } + private List photo(){ + if(this.photo == null) { + this.photo = new ArrayList<>(); + } + return this.photo; + } + private void clearPhoto(){ + if(this.photo != null) { + this.photo.clear(); + } + } + public final B photo(@Nullable AttachmentDTO photo) { + return this.photo(photo != null ? List.of(photo) : null); + } + public final B photo(@Nullable List photo) { + this.clearPhoto(); + return this.addPhoto(photo); + } + public final B addPhoto(@Nullable AttachmentDTO photo) { + return this.addPhoto(photo != null ? List.of(photo) : null); + } + public final B addPhoto(@Nullable List photo) { + if (photo != null) { + this.photo().addAll(photo); + } + return this.self(); + } + private List telecom(){ + if(this.telecom == null) { + this.telecom = new ArrayList<>(); + } + return this.telecom; + } + private void clearTelecom(){ + if(this.telecom != null) { + this.telecom.clear(); + } + } + public final B telecom(@Nullable ContactPointDTO telecom) { + return this.telecom(telecom != null ? List.of(telecom) : null); + } + public final B telecom(@Nullable List telecom) { + this.clearTelecom(); + return this.addTelecom(telecom); + } + public final B addTelecom(@Nullable ContactPointDTO telecom) { + return this.addTelecom(telecom != null ? List.of(telecom) : null); + } + public final B addTelecom(@Nullable List telecom) { + if (telecom != null) { + this.telecom().addAll(telecom); + } + return this.self(); + } + + protected abstract B self(); + } public static final class PatientDTOBuilder extends AbstractPatientDTOBuilder{ + private PatientDTOBuilder(){ + } + protected PatientDTOBuilder self(){ + return this; + } + public PatientDTO build(){ + return new PatientDTO(this); + } + } + private PatientDTO(PatientDTOBuilder builder) { + this((AbstractPatientDTOBuilder)requireNonNull(builder)); + } + + protected PatientDTO(AbstractPatientDTOBuilder builder) { + super(requireNonNull(builder)); + + + + + + + + + + + + + + + + + + + this.active = builder.active != null ? requireNonNull(builder.active.value()) : null; + this._active = builder.active != null ? builder.active.element() : null; + this.address = builder.address != null ? List.copyOf(builder.address) : null; + this.birthDate = builder.birthDate != null ? requireNonNull(builder.birthDate.value()) : null; + this._birthDate = builder.birthDate != null ? builder.birthDate.element() : null; + this.communication = builder.communication != null ? List.copyOf(builder.communication) : null; + this.contact = builder.contact != null ? List.copyOf(builder.contact) : null; + this.deceasedBoolean = builder.deceasedBoolean != null ? requireNonNull(builder.deceasedBoolean.value()) : null; + this._deceasedBoolean = builder.deceasedBoolean != null ? builder.deceasedBoolean.element() : null; + this.deceasedDateTime = builder.deceasedDateTime != null ? requireNonNull(builder.deceasedDateTime.value()) : null; + this._deceasedDateTime = builder.deceasedDateTime != null ? builder.deceasedDateTime.element() : null; + this.gender = builder.gender != null ? requireNonNull(builder.gender.value()) : null; + this._gender = builder.gender != null ? builder.gender.element() : null; + this.generalPractitioner = builder.generalPractitioner != null ? List.copyOf(builder.generalPractitioner) : null; + this.identifier = builder.identifier != null ? List.copyOf(builder.identifier) : null; + this.link = builder.link != null ? List.copyOf(builder.link) : null; + this.managingOrganization = builder.managingOrganization; + this.maritalStatus = builder.maritalStatus; + this.multipleBirthBoolean = builder.multipleBirthBoolean != null ? requireNonNull(builder.multipleBirthBoolean.value()) : null; + this._multipleBirthBoolean = builder.multipleBirthBoolean != null ? builder.multipleBirthBoolean.element() : null; + this.multipleBirthInteger = builder.multipleBirthInteger != null ? requireNonNull(builder.multipleBirthInteger.value()) : null; + this._multipleBirthInteger = builder.multipleBirthInteger != null ? builder.multipleBirthInteger.element() : null; + this.name = builder.name != null ? List.copyOf(builder.name) : null; + this.photo = builder.photo != null ? List.copyOf(builder.photo) : null; + this.telecom = builder.telecom != null ? List.copyOf(builder.telecom) : null; + } + protected > B applyToBuilder(B builder) { + requireNonNull(builder); + super.applyToBuilder(builder); + builder + .active(this.active,this._active) + .address(this.address) + .birthDate(this.birthDate,this._birthDate) + .communication(this.communication) + .contact(this.contact) + .deceasedBoolean(this.deceasedBoolean,this._deceasedBoolean) + .deceasedDateTime(this.deceasedDateTime,this._deceasedDateTime) + .gender(this.gender,this._gender) + .generalPractitioner(this.generalPractitioner) + .identifier(this.identifier) + .link(this.link) + .managingOrganization(this.managingOrganization) + .maritalStatus(this.maritalStatus) + .multipleBirthBoolean(this.multipleBirthBoolean,this._multipleBirthBoolean) + .multipleBirthInteger(this.multipleBirthInteger,this._multipleBirthInteger) + .name(this.name) + .photo(this.photo) + .telecom(this.telecom) + ; + return builder; + } + public final PatientDTOBuilder toBuilder() { + return this.applyToBuilder(PatientDTO.patientDtoBuilder()); + } + + public int hashCode() { + return Objects.hash(super.hashCode(),this.active, this.address, this.birthDate, this.communication, this.contact, this.deceasedBoolean, this.deceasedDateTime, this.gender, this.generalPractitioner, this.identifier, this.link, this.managingOrganization, this.maritalStatus, this.multipleBirthBoolean, this.multipleBirthInteger, this.name, this.photo, this.telecom); + } + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (obj == null) { + return false; + }else if(!super.equals(obj)) { + return false; + } else if (!(obj instanceof PatientDTO)) { + return false; + } + final PatientDTO other = (PatientDTO) obj; + return Utils.equals(this.active, other.active) && Utils.equals(this.address, other.address) && Utils.equals(this.birthDate, other.birthDate) && Utils.equals(this.communication, other.communication) && Utils.equals(this.contact, other.contact) && Utils.equals(this.deceasedBoolean, other.deceasedBoolean) && Utils.equals(this.deceasedDateTime, other.deceasedDateTime) && Utils.equals(this.gender, other.gender) && Utils.equals(this.generalPractitioner, other.generalPractitioner) && Utils.equals(this.identifier, other.identifier) && Utils.equals(this.link, other.link) && Utils.equals(this.managingOrganization, other.managingOrganization) && Utils.equals(this.maritalStatus, other.maritalStatus) && Utils.equals(this.multipleBirthBoolean, other.multipleBirthBoolean) && Utils.equals(this.multipleBirthInteger, other.multipleBirthInteger) && Utils.equals(this.name, other.name) && Utils.equals(this.photo, other.photo) && Utils.equals(this.telecom, other.telecom); + } + protected Utils.ToStringHelper toStringHelper(){ + return super.toStringHelper() + .add("active", this.active) + .add("_active", this._active) + .add("address", this.address) + .add("birthDate", this.birthDate) + .add("_birthDate", this._birthDate) + .add("communication", this.communication) + .add("contact", this.contact) + .add("deceasedBoolean", this.deceasedBoolean) + .add("_deceasedBoolean", this._deceasedBoolean) + .add("deceasedDateTime", this.deceasedDateTime) + .add("_deceasedDateTime", this._deceasedDateTime) + .add("gender", this.gender) + .add("_gender", this._gender) + .add("generalPractitioner", this.generalPractitioner) + .add("identifier", this.identifier) + .add("link", this.link) + .add("managingOrganization", this.managingOrganization) + .add("maritalStatus", this.maritalStatus) + .add("multipleBirthBoolean", this.multipleBirthBoolean) + .add("_multipleBirthBoolean", this._multipleBirthBoolean) + .add("multipleBirthInteger", this.multipleBirthInteger) + .add("_multipleBirthInteger", this._multipleBirthInteger) + .add("name", this.name) + .add("photo", this.photo) + .add("telecom", this.telecom) + ; + } + +}" +`; diff --git a/test/api/mustache.test.ts b/test/api/mustache.test.ts new file mode 100644 index 000000000..02887a33c --- /dev/null +++ b/test/api/mustache.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from "bun:test"; +import { APIBuilder } from "@root/api/builder"; +import { r4Manager } from "@typeschema-test/utils"; + +describe("Mustache Template Based Generation", async () => { + const report = await new APIBuilder({ manager: r4Manager }) + .setLogLevel("SILENT") + .mustache("./examples/mustache/java", { + debug: "COMPACT", + inMemoryOnly: true, + shouldRunHooks: false, + meta: { + timestamp: "2025-12-24T00:00:00.000Z", + }, + }) + .throwException() + .generate(); + expect(report.success).toBeTrue(); + expect(Object.keys(report.filesGenerated).length).toEqual(192); + it("Patient resource", async () => { + expect( + report.filesGenerated["generated/model/src/main/java/de/solutio/fhir/models/resources/PatientDTO.java"], + ).toMatchSnapshot(); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 22e6e3180..d2cca5b0f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,7 +26,8 @@ "paths": { "@root/*": ["./src/*"], "@typeschema/*": ["./src/typeschema/*"], - "@typeschema-test/*": ["./test/unit/typeschema/*"] + "@typeschema-test/*": ["./test/unit/typeschema/*"], + "@mustache/*": ["./src/api/mustache/*"], } }, "include": ["./src", "./test"],