modelable 1.0.1__tar.gz → 1.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of modelable might be problematic. Click here for more details.
- {modelable-1.0.1 → modelable-1.0.2}/PKG-INFO +1 -1
- {modelable-1.0.1 → modelable-1.0.2}/pyproject.toml +1 -1
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/emitters/rust.py +131 -26
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/emitters/typescript.py +3 -4
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_emit_rust.py +204 -8
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_emit_typescript.py +36 -5
- {modelable-1.0.1 → modelable-1.0.2}/uv.lock +1 -1
- {modelable-1.0.1 → modelable-1.0.2}/.gitignore +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/.python-version +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/LICENSE +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/README.md +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/__init__.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/__main__.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/_pydantic_py314_compat.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/cli.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/commands/__init__.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/commands/apicurio.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/commands/codegen.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/commands/common.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/commands/compile.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/commands/create.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/commands/diff.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/commands/graph.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/commands/llm.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/commands/lsp.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/commands/runtime.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/commands/scenario.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/commands/spec.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/commands/workspace.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/compat/__init__.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/compat/checker.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/compat/diff.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/compiler/__init__.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/compiler/compiler.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/compiler/workspace.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/diagnostics/__init__.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/diagnostics/model.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/emitters/__init__.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/emitters/base.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/emitters/csharp.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/emitters/dbt_yaml.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/emitters/diagnostics.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/emitters/fhir.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/emitters/fhir_validator.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/emitters/go.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/emitters/java.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/emitters/json_schema.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/emitters/markdown.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/emitters/odcs.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/emitters/openlineage.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/emitters/openmetadata.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/emitters/python.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/emitters/shapes.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/emitters/sql.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/emitters/targets.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/expressions/__init__.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/expressions/cel.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/governance/__init__.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/governance/checker.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/governance/por.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/grammar/__init__.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/grammar/modelable.lark +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/graph/__init__.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/graph/export.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/llm/__init__.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/llm/chat.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/llm/config.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/llm/context.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/llm/engine.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/llm/importers.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/llm/provenance.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/llm/providers.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/llm/qa.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/llm/recommendations.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/llm/redaction.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/llm/render.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/llm/update_plan.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/llm/validation_help.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/lsp/__init__.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/lsp/__main__.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/lsp/code_actions.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/lsp/completion.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/lsp/definition.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/lsp/diagnostics.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/lsp/document_symbols.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/lsp/federation.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/lsp/folding.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/lsp/formatting.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/lsp/highlight.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/lsp/hover.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/lsp/inlay_hints.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/lsp/references.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/lsp/rename.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/lsp/semantic_tokens.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/lsp/server.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/lsp/workspace.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/lsp/workspace_symbols.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/parser/__init__.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/parser/ir.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/parser/parse.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/parser/transformer.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/parser/wire.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/planner/__init__.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/planner/lineage.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/planner/planner.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/planner/plans.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/py.typed +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/registry/__init__.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/registry/apicurio.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/registry/base.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/registry/factory.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/registry/index.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/registry/local.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/registry/oci.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/registry/resolver.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/registry/schema.sql +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/registry/signature.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/release.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/runtime/__init__.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/runtime/adapter/__init__.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/runtime/adapter/base.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/runtime/adapter/postgres.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/specs/__init__.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/specs/tracking.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/validation/__init__.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/src/modelable/validation/semantic.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/conftest.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/fixtures/auto_projection_complex.mdl +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/fixtures/billing_projection.mdl +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/fixtures/breaking_changes.mdl +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/fixtures/cel_error_cases.mdl +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/fixtures/customer.mdl +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/fixtures/federation_multi_registry/consumer.mdl +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/fixtures/federation_multi_registry/provider.mdl +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/fixtures/federation_multi_registry/workspace.mdl +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/fixtures/fhir_patient_profile.mdl +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/fixtures/governance_export_model.mdl +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/fixtures/materialized_projection_chain.mdl +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/fixtures/missing_header.mdl +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/fixtures/multi_domain_joins.mdl +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/fixtures/multi_language_target.mdl +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/fixtures/no_owner.mdl +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/fixtures/pii_governance.mdl +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/fixtures/projection_of_projection.mdl +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/fixtures/sql_and_dbt_targets.mdl +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/helpers.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_apicurio_registry.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_apicurio_testcontainers.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_auto_projection.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_cel_validation.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_cli.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_cli_create.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_cli_inspect.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_codegen_docker_smoke.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_codegen_targets.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_compatibility.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_compiler.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_conformance_fixture.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_diagnostics.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_emit_csharp.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_emit_dbt_yaml.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_emit_fhir.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_emit_go.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_emit_java.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_emit_json_schema.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_emit_markdown.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_emit_odcs.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_emit_openlineage.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_emit_openmetadata.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_emit_python.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_emit_sql.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_fhir_validator.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_fixtures_regression.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_governance.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_grammar.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_graph_export.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_impact_analysis.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_lineage.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_llm_features.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_llm_provider_integration.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_lsp_code_actions.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_lsp_completion.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_lsp_completion_mirrors.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_lsp_definition.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_lsp_diagnostics.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_lsp_document_symbols.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_lsp_features.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_lsp_federation_diagnostics.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_lsp_folding.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_lsp_formatting.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_lsp_highlight.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_lsp_hover.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_lsp_inlay_hints.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_lsp_integration.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_lsp_references.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_lsp_rename.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_lsp_semantic_tokens.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_lsp_server.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_lsp_workspace.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_lsp_workspace_symbols.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_openmetadata_testcontainers.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_planner.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_por.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_postgres_adapter.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_py314_compat.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_registry.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_registry_index.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_release_metadata.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_release_workflow.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_repository_release.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_resolver.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_samples.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_semantic.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_serialization_hints.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_spec_tracking.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_transformer.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_validate_surface_detection.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_version_range_resolution.py +0 -0
- {modelable-1.0.1 → modelable-1.0.2}/tests/test_workspace.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: modelable
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.2
|
|
4
4
|
Summary: Compiler and language server for versioned, domain-owned data models
|
|
5
5
|
Project-URL: Homepage, https://github.com/ktjn/modelable
|
|
6
6
|
Project-URL: Documentation, https://github.com/ktjn/modelable/tree/main/docs
|
|
@@ -9,7 +9,16 @@ from modelable.compiler.workspace import Workspace
|
|
|
9
9
|
from modelable.emitters.base import EmittedArtifact, compute_content_hash
|
|
10
10
|
from modelable.emitters.diagnostics import missing_metadata, type_loss
|
|
11
11
|
from modelable.emitters.shapes import TypeShape
|
|
12
|
-
from modelable.parser.ir import
|
|
12
|
+
from modelable.parser.ir import (
|
|
13
|
+
ArrayType,
|
|
14
|
+
DirectMapping,
|
|
15
|
+
DomainDef,
|
|
16
|
+
MapType,
|
|
17
|
+
MdlFile,
|
|
18
|
+
ModelVersion,
|
|
19
|
+
NamedType,
|
|
20
|
+
ProjectionVersion,
|
|
21
|
+
)
|
|
13
22
|
from modelable.registry.resolver import resolve_model_ref
|
|
14
23
|
|
|
15
24
|
|
|
@@ -27,33 +36,35 @@ def _append_cross_enum_from_impls(
|
|
|
27
36
|
enum_registry: dict[str, dict],
|
|
28
37
|
) -> list[EmittedArtifact]:
|
|
29
38
|
"""For each pair of enum types with identical variants in the same domain,
|
|
30
|
-
append From impl blocks
|
|
39
|
+
append From impl blocks into projection files without manual match arms.
|
|
31
40
|
"""
|
|
32
|
-
# Group by domain: domain -> [(artifact_id, module_name, enums_dict)]
|
|
33
|
-
by_domain: dict[str, list[tuple[str, str, dict[str, list[str]]]]] = {}
|
|
41
|
+
# Group by domain: domain -> [(artifact_id, module_name, enums_dict, kind)]
|
|
42
|
+
by_domain: dict[str, list[tuple[str, str, dict[str, list[str]], str]]] = {}
|
|
34
43
|
for art_id, info in enum_registry.items():
|
|
35
44
|
domain = info["domain"]
|
|
36
|
-
by_domain.setdefault(domain, []).append((art_id, info["module_name"], info["enums"]))
|
|
45
|
+
by_domain.setdefault(domain, []).append((art_id, info["module_name"], info["enums"], info["kind"]))
|
|
37
46
|
|
|
38
47
|
# Build: frozenset(raw_variants) -> [(artifact_id, module_name, enum_type_name)]
|
|
39
48
|
# Per domain — From impls only work within the same Rust module tree (super:: path).
|
|
40
49
|
extra: dict[str, list[str]] = {} # artifact_id -> lines to append
|
|
41
50
|
for _domain, entries in by_domain.items():
|
|
42
|
-
variant_map: dict[frozenset, list[tuple[str, str, str]]] = {}
|
|
43
|
-
for art_id, module_name, enums in entries:
|
|
51
|
+
variant_map: dict[frozenset, list[tuple[str, str, str, str]]] = {}
|
|
52
|
+
for art_id, module_name, enums, kind in entries:
|
|
44
53
|
for enum_type_name, raw_variants in enums.items():
|
|
45
54
|
key = frozenset(raw_variants)
|
|
46
|
-
variant_map.setdefault(key, []).append((art_id, module_name, enum_type_name))
|
|
55
|
+
variant_map.setdefault(key, []).append((art_id, module_name, enum_type_name, kind))
|
|
47
56
|
|
|
48
57
|
for variant_set, enum_list in variant_map.items():
|
|
49
58
|
if len(enum_list) < 2:
|
|
50
59
|
continue
|
|
51
60
|
sorted_variants = sorted(variant_set)
|
|
52
61
|
# For each ordered pair (src → tgt): put From<src> for tgt in tgt's file.
|
|
53
|
-
for src_art_id, src_module, src_enum in enum_list:
|
|
54
|
-
for tgt_art_id, _tgt_module, tgt_enum in enum_list:
|
|
62
|
+
for src_art_id, src_module, src_enum, _src_kind in enum_list:
|
|
63
|
+
for tgt_art_id, _tgt_module, tgt_enum, tgt_kind in enum_list:
|
|
55
64
|
if src_art_id == tgt_art_id and src_enum == tgt_enum:
|
|
56
65
|
continue
|
|
66
|
+
if tgt_kind == "model":
|
|
67
|
+
continue
|
|
57
68
|
lines: list[str] = [
|
|
58
69
|
"",
|
|
59
70
|
f"use super::{src_module}::{src_enum};",
|
|
@@ -100,7 +111,9 @@ def emit_rust(workspace: Workspace, out_dir: Path) -> list[EmittedArtifact]:
|
|
|
100
111
|
for domain in workspace.mdl.domains:
|
|
101
112
|
for model_name, versions in domain.models.items():
|
|
102
113
|
for version in versions:
|
|
103
|
-
artifacts.append(
|
|
114
|
+
artifacts.append(
|
|
115
|
+
_emit_model(domain, model_name, version, out_dir, enum_registry=enum_registry, mdl=workspace.mdl)
|
|
116
|
+
)
|
|
104
117
|
for projection_name, versions in domain.projections.items():
|
|
105
118
|
for version in versions:
|
|
106
119
|
source = version.source.model
|
|
@@ -157,6 +170,40 @@ def _stable_type_name(domain: str, name: str, version: int) -> str:
|
|
|
157
170
|
return f"{_pascalize(domain)}{_pascalize(name)}V{version}"
|
|
158
171
|
|
|
159
172
|
|
|
173
|
+
def _collect_named_type_refs(field_type, result: set) -> None:
|
|
174
|
+
"""Recursively collect NamedType names from a field type."""
|
|
175
|
+
if isinstance(field_type, NamedType):
|
|
176
|
+
result.add(field_type.name)
|
|
177
|
+
elif isinstance(field_type, ArrayType):
|
|
178
|
+
_collect_named_type_refs(field_type.item, result)
|
|
179
|
+
elif isinstance(field_type, MapType):
|
|
180
|
+
_collect_named_type_refs(field_type.key, result)
|
|
181
|
+
_collect_named_type_refs(field_type.value, result)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _resolve_named_type_map(named_refs: set, mdl: MdlFile | None) -> tuple[dict[str, str], list[str]]:
|
|
185
|
+
"""Resolve NamedType references to Rust type names from the workspace.
|
|
186
|
+
|
|
187
|
+
Returns (name -> rust_type_name, list of use statements).
|
|
188
|
+
"""
|
|
189
|
+
if not named_refs or mdl is None:
|
|
190
|
+
return {}, []
|
|
191
|
+
resolved_map: dict[str, str] = {}
|
|
192
|
+
use_statements: list[str] = []
|
|
193
|
+
for name in named_refs:
|
|
194
|
+
for domain in mdl.domains:
|
|
195
|
+
if name in domain.models:
|
|
196
|
+
versions = domain.models[name]
|
|
197
|
+
if versions:
|
|
198
|
+
latest = versions[-1]
|
|
199
|
+
rust_name = _stable_type_name(domain.name, name, latest.version)
|
|
200
|
+
module = _snake_case(rust_name)
|
|
201
|
+
resolved_map[name] = rust_name
|
|
202
|
+
use_statements.append(f"use super::{module}::{rust_name};")
|
|
203
|
+
break
|
|
204
|
+
return resolved_map, use_statements
|
|
205
|
+
|
|
206
|
+
|
|
160
207
|
def _emit_model(
|
|
161
208
|
domain: DomainDef,
|
|
162
209
|
model_name: str,
|
|
@@ -164,30 +211,46 @@ def _emit_model(
|
|
|
164
211
|
out_dir: Path,
|
|
165
212
|
*,
|
|
166
213
|
enum_registry: dict[str, dict] | None = None,
|
|
214
|
+
mdl: MdlFile | None = None,
|
|
167
215
|
) -> EmittedArtifact:
|
|
168
216
|
artifact_id = _artifact_id(domain.name, model_name, version.version)
|
|
169
217
|
type_name = _stable_type_name(domain.name, model_name, version.version)
|
|
170
218
|
nested_definitions: dict[str, list[str]] = {}
|
|
171
219
|
local_enum_info: dict[str, list[str]] = {}
|
|
220
|
+
|
|
221
|
+
# Resolve NamedType references from the workspace
|
|
222
|
+
named_refs: set[str] = set()
|
|
223
|
+
for field in version.fields:
|
|
224
|
+
_collect_named_type_refs(field.type, named_refs)
|
|
225
|
+
named_type_map, use_statements = _resolve_named_type_map(named_refs, mdl)
|
|
226
|
+
|
|
172
227
|
field_specs = _field_specs_from_model_fields(
|
|
173
|
-
version.fields,
|
|
228
|
+
version.fields,
|
|
229
|
+
owner_type=type_name,
|
|
230
|
+
path=[],
|
|
231
|
+
definitions=nested_definitions,
|
|
232
|
+
enum_info=local_enum_info,
|
|
233
|
+
named_type_map=named_type_map,
|
|
174
234
|
)
|
|
175
235
|
if enum_registry is not None:
|
|
176
236
|
enum_registry[artifact_id] = {
|
|
177
237
|
"enums": local_enum_info,
|
|
178
238
|
"module_name": _snake_case(type_name),
|
|
179
239
|
"domain": domain.name,
|
|
240
|
+
"kind": "model",
|
|
180
241
|
}
|
|
181
242
|
|
|
182
243
|
warnings: list[str] = []
|
|
183
244
|
for field in version.fields:
|
|
184
|
-
if isinstance(field.type, NamedType):
|
|
245
|
+
if isinstance(field.type, NamedType) and field.type.name not in named_type_map:
|
|
185
246
|
warnings.append(missing_metadata(f"{domain.name}.{model_name}.{field.name}"))
|
|
186
247
|
|
|
187
248
|
needs_serde_with = _any_needs_serde_with(field_specs)
|
|
188
249
|
needs_uuid = _any_needs_uuid(field_specs)
|
|
189
250
|
needs_serde_json = _any_needs_serde_json(field_specs)
|
|
190
|
-
lines = _header_lines(
|
|
251
|
+
lines = _header_lines(
|
|
252
|
+
serde_with=needs_serde_with, uuid=needs_uuid, serde_json=needs_serde_json, extra_uses=use_statements
|
|
253
|
+
)
|
|
191
254
|
lines.extend(_render_struct_definition(type_name, field_specs))
|
|
192
255
|
lines.extend(_render_nested_definitions(nested_definitions))
|
|
193
256
|
|
|
@@ -228,18 +291,23 @@ def _emit_projection(
|
|
|
228
291
|
field_specs.append(_FieldSpec(index=index, name=field.name, annotation="String", optional=False))
|
|
229
292
|
continue
|
|
230
293
|
wire = _resolve_merged_projection_wire(field, version, mdl)
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
294
|
+
if clickhouse_row and field_shape.kind == "enum":
|
|
295
|
+
# clickhouse-rs 0.15 panics on serialize_unit_variant for typed enums;
|
|
296
|
+
# force String for all ClickHouse-bound enum fields.
|
|
297
|
+
annotation = "String"
|
|
298
|
+
else:
|
|
299
|
+
annotation = _shape_annotation(
|
|
300
|
+
field_shape,
|
|
301
|
+
owner_type=type_name,
|
|
302
|
+
path=[field.name],
|
|
303
|
+
definitions=nested_definitions,
|
|
304
|
+
rust_hint=wire.get("rust"),
|
|
305
|
+
clickhouse_hint=wire.get("clickhouse"),
|
|
306
|
+
enum_info=local_enum_info,
|
|
307
|
+
)
|
|
240
308
|
optional = field_shape.optional or field_shape.nullable
|
|
241
309
|
serde_attrs = _serde_attrs_for_field(wire, field_shape, clickhouse=clickhouse_row)
|
|
242
|
-
if field_shape.optional:
|
|
310
|
+
if field_shape.optional and not clickhouse_row:
|
|
243
311
|
serde_attrs = ['#[serde(skip_serializing_if = "Option::is_none")]', *serde_attrs]
|
|
244
312
|
field_specs.append(
|
|
245
313
|
_FieldSpec(index=index, name=field.name, annotation=annotation, optional=optional, serde_attrs=serde_attrs)
|
|
@@ -267,13 +335,18 @@ def _emit_projection(
|
|
|
267
335
|
_render_struct_definition(type_name, field_specs, extra_derives=extra_derives, storage_gated=storage_gated)
|
|
268
336
|
)
|
|
269
337
|
lines.extend(_render_nested_definitions(nested_definitions))
|
|
270
|
-
lines.extend(
|
|
338
|
+
lines.extend(
|
|
339
|
+
_emit_from_impl(
|
|
340
|
+
type_name, domain.name, version, mdl, storage_gated=storage_gated, clickhouse_row=clickhouse_row
|
|
341
|
+
)
|
|
342
|
+
)
|
|
271
343
|
|
|
272
344
|
if enum_registry is not None:
|
|
273
345
|
enum_registry[artifact_id] = {
|
|
274
346
|
"enums": local_enum_info,
|
|
275
347
|
"module_name": _snake_case(type_name),
|
|
276
348
|
"domain": domain.name,
|
|
349
|
+
"kind": "projection",
|
|
277
350
|
}
|
|
278
351
|
|
|
279
352
|
text = "\n".join(lines) + "\n"
|
|
@@ -320,6 +393,7 @@ def _emit_from_impl(
|
|
|
320
393
|
mdl: MdlFile,
|
|
321
394
|
*,
|
|
322
395
|
storage_gated: bool = False,
|
|
396
|
+
clickhouse_row: bool = False,
|
|
323
397
|
) -> list[str]:
|
|
324
398
|
"""Emit impl From<SourceModel> for Projection.
|
|
325
399
|
|
|
@@ -352,6 +426,16 @@ def _emit_from_impl(
|
|
|
352
426
|
if storage_gated:
|
|
353
427
|
lines.append('#[cfg(feature = "storage")]')
|
|
354
428
|
lines.append(f"use super::{src_module}::{src_type_name};")
|
|
429
|
+
if clickhouse_row:
|
|
430
|
+
for proj_field in version.fields:
|
|
431
|
+
if not isinstance(proj_field.mapping, DirectMapping):
|
|
432
|
+
continue
|
|
433
|
+
field_shape = _resolve_projection_field_shape(proj_field, version, mdl)
|
|
434
|
+
if field_shape is not None and field_shape.kind == "enum":
|
|
435
|
+
enum_type = _nested_type_name(src_type_name, [proj_field.mapping.source_field])
|
|
436
|
+
if storage_gated:
|
|
437
|
+
lines.append('#[cfg(feature = "storage")]')
|
|
438
|
+
lines.append(f"use super::{src_module}::{enum_type};")
|
|
355
439
|
if storage_gated:
|
|
356
440
|
lines.append('#[cfg(feature = "storage")]')
|
|
357
441
|
lines.append(f"impl From<{src_type_name}> for {proj_type_name} {{")
|
|
@@ -363,7 +447,15 @@ def _emit_from_impl(
|
|
|
363
447
|
if isinstance(proj_field.mapping, DirectMapping):
|
|
364
448
|
src_rust_name = _field_name(proj_field.mapping.source_field)
|
|
365
449
|
field_shape = _resolve_projection_field_shape(proj_field, version, mdl)
|
|
366
|
-
if
|
|
450
|
+
if clickhouse_row and field_shape is not None and field_shape.kind == "enum":
|
|
451
|
+
# ClickHouse-bound enum fields are stored as String; generate explicit match.
|
|
452
|
+
src_enum_type = _nested_type_name(src_type_name, [proj_field.mapping.source_field])
|
|
453
|
+
lines.append(f" {rust_name}: match src.{src_rust_name} {{")
|
|
454
|
+
for raw_v in field_shape.enum_values:
|
|
455
|
+
member = _enum_member_name(raw_v)
|
|
456
|
+
lines.append(f' {src_enum_type}::{member} => "{raw_v}".to_string(),')
|
|
457
|
+
lines.append(" },")
|
|
458
|
+
elif _projection_field_is_json_passthrough_to_string(proj_field, version, mdl):
|
|
367
459
|
lines.append(
|
|
368
460
|
f" {rust_name}: serde_json::to_string(&src.{src_rust_name}).unwrap_or_default(),"
|
|
369
461
|
)
|
|
@@ -435,6 +527,7 @@ def _header_lines(
|
|
|
435
527
|
clickhouse: bool = False,
|
|
436
528
|
uuid: bool = False,
|
|
437
529
|
serde_json: bool = False,
|
|
530
|
+
extra_uses: list[str] | None = None,
|
|
438
531
|
) -> list[str]:
|
|
439
532
|
lines = [
|
|
440
533
|
"// @generated by Modelable",
|
|
@@ -451,6 +544,9 @@ def _header_lines(
|
|
|
451
544
|
lines.insert(1, "// requires: uuid (https://docs.rs/uuid)")
|
|
452
545
|
if serde_json:
|
|
453
546
|
lines.insert(1, "// requires: serde_json (https://docs.rs/serde_json)")
|
|
547
|
+
if extra_uses:
|
|
548
|
+
# Insert use statements just before the trailing empty string
|
|
549
|
+
lines[-1:-1] = extra_uses
|
|
454
550
|
return lines
|
|
455
551
|
|
|
456
552
|
|
|
@@ -540,6 +636,7 @@ def _field_specs_from_model_fields(
|
|
|
540
636
|
path: list[str],
|
|
541
637
|
definitions: dict[str, list[str]],
|
|
542
638
|
enum_info: dict[str, list[str]] | None = None,
|
|
639
|
+
named_type_map: dict[str, str] | None = None,
|
|
543
640
|
) -> list[_FieldSpec]:
|
|
544
641
|
specs: list[_FieldSpec] = []
|
|
545
642
|
for index, field in enumerate(fields):
|
|
@@ -552,6 +649,7 @@ def _field_specs_from_model_fields(
|
|
|
552
649
|
definitions=definitions,
|
|
553
650
|
rust_hint=wire.get("rust"),
|
|
554
651
|
enum_info=enum_info,
|
|
652
|
+
named_type_map=named_type_map,
|
|
555
653
|
)
|
|
556
654
|
is_optional = shape.optional or shape.nullable
|
|
557
655
|
serde_attrs = _serde_attrs_for_field(wire, shape)
|
|
@@ -616,6 +714,7 @@ def _shape_annotation(
|
|
|
616
714
|
rust_hint=None,
|
|
617
715
|
clickhouse_hint=None,
|
|
618
716
|
enum_info: dict[str, list[str]] | None = None,
|
|
717
|
+
named_type_map: dict[str, str] | None = None,
|
|
619
718
|
) -> str:
|
|
620
719
|
base = _shape_base_annotation(
|
|
621
720
|
shape,
|
|
@@ -625,6 +724,7 @@ def _shape_annotation(
|
|
|
625
724
|
rust_hint=rust_hint,
|
|
626
725
|
clickhouse_hint=clickhouse_hint,
|
|
627
726
|
enum_info=enum_info,
|
|
727
|
+
named_type_map=named_type_map,
|
|
628
728
|
)
|
|
629
729
|
if shape.optional or shape.nullable:
|
|
630
730
|
return f"Option<{base}>"
|
|
@@ -640,6 +740,7 @@ def _shape_base_annotation(
|
|
|
640
740
|
rust_hint=None,
|
|
641
741
|
clickhouse_hint=None,
|
|
642
742
|
enum_info: dict[str, list[str]] | None = None,
|
|
743
|
+
named_type_map: dict[str, str] | None = None,
|
|
643
744
|
) -> str:
|
|
644
745
|
clickhouse_string = clickhouse_hint is not None and getattr(clickhouse_hint, "encoding", None) == "string"
|
|
645
746
|
if shape.kind == "primitive":
|
|
@@ -659,6 +760,7 @@ def _shape_base_annotation(
|
|
|
659
760
|
definitions=definitions,
|
|
660
761
|
rust_hint=rust_hint,
|
|
661
762
|
enum_info=enum_info,
|
|
763
|
+
named_type_map=named_type_map,
|
|
662
764
|
)
|
|
663
765
|
return f"Vec<{element_type}>"
|
|
664
766
|
if shape.kind == "map":
|
|
@@ -671,6 +773,7 @@ def _shape_base_annotation(
|
|
|
671
773
|
path=[*path, "Value"],
|
|
672
774
|
definitions=definitions,
|
|
673
775
|
enum_info=enum_info,
|
|
776
|
+
named_type_map=named_type_map,
|
|
674
777
|
)
|
|
675
778
|
return f"HashMap<String, {value_type}>"
|
|
676
779
|
if shape.kind == "ref":
|
|
@@ -683,6 +786,8 @@ def _shape_base_annotation(
|
|
|
683
786
|
enum_info[enum_type_name] = list(shape.enum_values)
|
|
684
787
|
return enum_type_name
|
|
685
788
|
if shape.kind == "named":
|
|
789
|
+
if named_type_map is not None and shape.ref in named_type_map:
|
|
790
|
+
return named_type_map[shape.ref]
|
|
686
791
|
return _pascalize(shape.ref or "Named")
|
|
687
792
|
if shape.kind == "object":
|
|
688
793
|
type_name = _nested_type_name(owner_type, path)
|
|
@@ -93,7 +93,7 @@ def _collect_ref_imports(field_type, mdl, resolved_refs: dict[str, str]) -> None
|
|
|
93
93
|
|
|
94
94
|
|
|
95
95
|
def _collect_named_imports(field_type, mdl, named_imports: dict[str, tuple[str, str]]) -> None:
|
|
96
|
-
"""Recursively resolve NamedType refs to (
|
|
96
|
+
"""Recursively resolve NamedType refs to (exported_alias, artifact_id) in named_imports."""
|
|
97
97
|
if isinstance(field_type, NamedType):
|
|
98
98
|
name = field_type.name
|
|
99
99
|
if name not in named_imports and mdl is not None:
|
|
@@ -102,9 +102,8 @@ def _collect_named_imports(field_type, mdl, named_imports: dict[str, tuple[str,
|
|
|
102
102
|
versions = domain.models[name]
|
|
103
103
|
if versions:
|
|
104
104
|
latest = max(versions, key=lambda v: v.version)
|
|
105
|
-
iface = _stable_interface_name(domain.name, name, latest.version)
|
|
106
105
|
aid = _artifact_id(domain.name, name, latest.version)
|
|
107
|
-
named_imports[name] = (
|
|
106
|
+
named_imports[name] = (name, aid)
|
|
108
107
|
return
|
|
109
108
|
elif isinstance(field_type, ArrayType):
|
|
110
109
|
_collect_named_imports(field_type.item, mdl, named_imports)
|
|
@@ -160,7 +159,7 @@ def _emit_model(domain: DomainDef, model_name: str, version: ModelVersion, out_d
|
|
|
160
159
|
body_lines.append("}")
|
|
161
160
|
body_lines.append(f"export type {model_name} = {interface_name};")
|
|
162
161
|
|
|
163
|
-
all_lines =
|
|
162
|
+
all_lines = meta_lines + ([*import_lines, ""] if import_lines else []) + body_lines
|
|
164
163
|
content = "\n".join(all_lines) + "\n"
|
|
165
164
|
return EmittedArtifact(
|
|
166
165
|
target="typescript",
|
|
@@ -461,6 +461,55 @@ binding span-row-binding {
|
|
|
461
461
|
assert '#[cfg(feature = "storage")]' not in model.content
|
|
462
462
|
|
|
463
463
|
|
|
464
|
+
def test_emit_rust_clickhouse_row_omits_skip_serializing_if_on_optional_fields(tmp_path):
|
|
465
|
+
"""Nullable ClickHouse row columns must serialize as NULL, not disappear."""
|
|
466
|
+
(tmp_path / "model.mdl").write_text(
|
|
467
|
+
"""
|
|
468
|
+
domain telemetry {
|
|
469
|
+
owner: "test-team"
|
|
470
|
+
entity Log @ 1 (additive) {
|
|
471
|
+
@key logId: uuid
|
|
472
|
+
traceId?: string
|
|
473
|
+
message: string
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
projection LogRow @ 1
|
|
477
|
+
from telemetry.Log @ 1 as l
|
|
478
|
+
{
|
|
479
|
+
logId <- l.logId
|
|
480
|
+
traceId <- l.traceId
|
|
481
|
+
message <- l.message
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
""",
|
|
485
|
+
encoding="utf-8",
|
|
486
|
+
)
|
|
487
|
+
(tmp_path / "bindings.mdl").write_text(
|
|
488
|
+
"""
|
|
489
|
+
binding ch-conn {
|
|
490
|
+
adapter: clickhouse
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
binding log-row-binding {
|
|
494
|
+
model: telemetry.Log @ 1
|
|
495
|
+
adapter: ch-conn
|
|
496
|
+
table: "logs"
|
|
497
|
+
}
|
|
498
|
+
""",
|
|
499
|
+
encoding="utf-8",
|
|
500
|
+
)
|
|
501
|
+
workspace = load_workspace(tmp_path)
|
|
502
|
+
artifacts = emit_rust(workspace, tmp_path / "out")
|
|
503
|
+
|
|
504
|
+
row = next(a for a in artifacts if a.ref == "telemetry.LogRow@1")
|
|
505
|
+
assert "clickhouse::Row" in row.content
|
|
506
|
+
assert "pub trace_id: Option<String>," in row.content
|
|
507
|
+
assert '#[serde(skip_serializing_if = "Option::is_none")]' not in row.content
|
|
508
|
+
|
|
509
|
+
model = next(a for a in artifacts if a.ref == "telemetry.Log@1")
|
|
510
|
+
assert '#[serde(skip_serializing_if = "Option::is_none")]' in model.content
|
|
511
|
+
|
|
512
|
+
|
|
464
513
|
def test_emit_rust_clickhouse_row_indirect_connector_binding(tmp_path):
|
|
465
514
|
"""Model binding via connector-binding name → adapter type resolved correctly for clickhouse."""
|
|
466
515
|
(tmp_path / "all.mdl").write_text(
|
|
@@ -1002,17 +1051,14 @@ domain catalog {
|
|
|
1002
1051
|
|
|
1003
1052
|
|
|
1004
1053
|
def test_emit_rust_warns_on_named_type_field(tmp_path):
|
|
1005
|
-
"""Rust emitter emits EMIT003 for NamedType field references (issue #120)."""
|
|
1054
|
+
"""Rust emitter emits EMIT003 for NamedType field references to unknown external types (issue #120)."""
|
|
1006
1055
|
(tmp_path / "test.mdl").write_text(
|
|
1007
1056
|
"""
|
|
1008
1057
|
domain customer {
|
|
1009
1058
|
owner: "test-team"
|
|
1010
|
-
value Address {
|
|
1011
|
-
line1: string
|
|
1012
|
-
}
|
|
1013
1059
|
entity Customer @ 1 (additive) {
|
|
1014
1060
|
@key customerId: uuid
|
|
1015
|
-
address:
|
|
1061
|
+
address: ExternalAddress
|
|
1016
1062
|
}
|
|
1017
1063
|
}
|
|
1018
1064
|
""",
|
|
@@ -1084,6 +1130,156 @@ domain tracing {
|
|
|
1084
1130
|
assert "impl From<TracingSpanV1SpanKind> for TracingSpanRowV1SpanKind" in proj_art.content
|
|
1085
1131
|
assert "use super::tracing_span_v1::TracingSpanV1SpanKind;" in proj_art.content
|
|
1086
1132
|
|
|
1087
|
-
#
|
|
1088
|
-
assert "
|
|
1089
|
-
assert "use super::tracing_span_row_v1
|
|
1133
|
+
# Domain model files must not import projection/storage enum types.
|
|
1134
|
+
assert "TracingSpanRowV1SpanKind" not in model_art.content
|
|
1135
|
+
assert "use super::tracing_span_row_v1" not in model_art.content
|
|
1136
|
+
|
|
1137
|
+
|
|
1138
|
+
def test_emit_rust_named_type_in_same_domain_resolves_without_warning(tmp_path):
|
|
1139
|
+
"""NamedType references to models in the same workspace resolve without EMIT003 (issue #120)."""
|
|
1140
|
+
(tmp_path / "test.mdl").write_text(
|
|
1141
|
+
"""
|
|
1142
|
+
domain nlq {
|
|
1143
|
+
owner: "test-team"
|
|
1144
|
+
value NlqTimeRange {
|
|
1145
|
+
from: string
|
|
1146
|
+
to: string
|
|
1147
|
+
}
|
|
1148
|
+
value NlqIr {
|
|
1149
|
+
timeRange: NlqTimeRange
|
|
1150
|
+
query: string
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
""",
|
|
1154
|
+
encoding="utf-8",
|
|
1155
|
+
)
|
|
1156
|
+
workspace = load_workspace(tmp_path)
|
|
1157
|
+
artifacts = emit_rust(workspace, tmp_path / "out")
|
|
1158
|
+
ir_art = next(a for a in artifacts if a.ref == "nlq.NlqIr@0")
|
|
1159
|
+
# The field must use the stable Rust type name (value types without @ N default to version 0)
|
|
1160
|
+
assert "pub time_range: NlqNlqTimeRangeV0," in ir_art.content
|
|
1161
|
+
# A use statement must be emitted
|
|
1162
|
+
assert "use super::nlq_nlq_time_range_v0::NlqNlqTimeRangeV0;" in ir_art.content
|
|
1163
|
+
# No EMIT003 warning
|
|
1164
|
+
assert not any("EMIT003" in w for w in ir_art.warnings)
|
|
1165
|
+
|
|
1166
|
+
|
|
1167
|
+
def test_emit_rust_named_type_array_in_same_domain_resolves(tmp_path):
|
|
1168
|
+
"""NamedType inside array<...> resolves to Vec<StableTypeName> (issue #120)."""
|
|
1169
|
+
(tmp_path / "test.mdl").write_text(
|
|
1170
|
+
"""
|
|
1171
|
+
domain nlq {
|
|
1172
|
+
owner: "test-team"
|
|
1173
|
+
value NlqFilter {
|
|
1174
|
+
field: string
|
|
1175
|
+
value: string
|
|
1176
|
+
}
|
|
1177
|
+
value NlqQuery {
|
|
1178
|
+
filters: array<NlqFilter>
|
|
1179
|
+
query: string
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
""",
|
|
1183
|
+
encoding="utf-8",
|
|
1184
|
+
)
|
|
1185
|
+
workspace = load_workspace(tmp_path)
|
|
1186
|
+
artifacts = emit_rust(workspace, tmp_path / "out")
|
|
1187
|
+
query_art = next(a for a in artifacts if a.ref == "nlq.NlqQuery@0")
|
|
1188
|
+
assert "pub filters: Vec<NlqNlqFilterV0>," in query_art.content
|
|
1189
|
+
assert "use super::nlq_nlq_filter_v0::NlqNlqFilterV0;" in query_art.content
|
|
1190
|
+
assert not any("EMIT003" in w for w in query_art.warnings)
|
|
1191
|
+
|
|
1192
|
+
|
|
1193
|
+
def test_emit_rust_clickhouse_row_enum_field_uses_string(tmp_path):
|
|
1194
|
+
"""ClickHouse-bound projection enum fields must be String, not typed enum (issue #119)."""
|
|
1195
|
+
(tmp_path / "model.mdl").write_text(
|
|
1196
|
+
"""
|
|
1197
|
+
domain telemetry {
|
|
1198
|
+
owner: "test-team"
|
|
1199
|
+
entity Span @ 1 (additive) {
|
|
1200
|
+
@key spanId: uuid
|
|
1201
|
+
spanKind: enum(INTERNAL, SERVER, CLIENT)
|
|
1202
|
+
name: string
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
projection SpanRow @ 1
|
|
1206
|
+
from telemetry.Span @ 1 as s
|
|
1207
|
+
{
|
|
1208
|
+
spanId <- s.spanId
|
|
1209
|
+
spanKind <- s.spanKind
|
|
1210
|
+
name <- s.name
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
""",
|
|
1214
|
+
encoding="utf-8",
|
|
1215
|
+
)
|
|
1216
|
+
(tmp_path / "bindings.mdl").write_text(
|
|
1217
|
+
"""
|
|
1218
|
+
binding ch-conn {
|
|
1219
|
+
adapter: clickhouse
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
binding span-binding {
|
|
1223
|
+
model: telemetry.Span @ 1
|
|
1224
|
+
adapter: ch-conn
|
|
1225
|
+
table: "spans"
|
|
1226
|
+
}
|
|
1227
|
+
""",
|
|
1228
|
+
encoding="utf-8",
|
|
1229
|
+
)
|
|
1230
|
+
workspace = load_workspace(tmp_path)
|
|
1231
|
+
artifacts = emit_rust(workspace, tmp_path / "out")
|
|
1232
|
+
proj = next(a for a in artifacts if a.ref == "telemetry.SpanRow@1")
|
|
1233
|
+
# Enum field must be String in the ClickHouse projection
|
|
1234
|
+
assert "pub span_kind: String," in proj.content
|
|
1235
|
+
# No typed enum definition for the projection
|
|
1236
|
+
assert "pub enum TelemetrySpanRowV1SpanKind" not in proj.content
|
|
1237
|
+
# Ensure typed enum is not used as field type
|
|
1238
|
+
assert "pub span_kind: TelemetrySpanV1SpanKind," not in proj.content
|
|
1239
|
+
|
|
1240
|
+
|
|
1241
|
+
def test_emit_rust_clickhouse_row_from_impl_enum_becomes_match_arm(tmp_path):
|
|
1242
|
+
"""From impl for ClickHouse row with enum fields uses explicit match → String (issue #119)."""
|
|
1243
|
+
(tmp_path / "model.mdl").write_text(
|
|
1244
|
+
"""
|
|
1245
|
+
domain telemetry {
|
|
1246
|
+
owner: "test-team"
|
|
1247
|
+
entity Span @ 1 (additive) {
|
|
1248
|
+
@key spanId: uuid
|
|
1249
|
+
spanKind: enum(INTERNAL, SERVER, CLIENT)
|
|
1250
|
+
name: string
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
projection SpanRow @ 1
|
|
1254
|
+
from telemetry.Span @ 1 as s
|
|
1255
|
+
{
|
|
1256
|
+
spanId <- s.spanId
|
|
1257
|
+
spanKind <- s.spanKind
|
|
1258
|
+
name <- s.name
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
""",
|
|
1262
|
+
encoding="utf-8",
|
|
1263
|
+
)
|
|
1264
|
+
(tmp_path / "bindings.mdl").write_text(
|
|
1265
|
+
"""
|
|
1266
|
+
binding ch-conn {
|
|
1267
|
+
adapter: clickhouse
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
binding span-binding {
|
|
1271
|
+
model: telemetry.Span @ 1
|
|
1272
|
+
adapter: ch-conn
|
|
1273
|
+
table: "spans"
|
|
1274
|
+
}
|
|
1275
|
+
""",
|
|
1276
|
+
encoding="utf-8",
|
|
1277
|
+
)
|
|
1278
|
+
workspace = load_workspace(tmp_path)
|
|
1279
|
+
artifacts = emit_rust(workspace, tmp_path / "out")
|
|
1280
|
+
proj = next(a for a in artifacts if a.ref == "telemetry.SpanRow@1")
|
|
1281
|
+
# Match arm generated for enum → String conversion
|
|
1282
|
+
assert "span_kind: match src.span_kind {" in proj.content
|
|
1283
|
+
assert 'TelemetrySpanV1SpanKind::INTERNAL => "INTERNAL".to_string(),' in proj.content
|
|
1284
|
+
# Must NOT use .into() for the enum field
|
|
1285
|
+
assert "span_kind: src.span_kind.into()," not in proj.content
|
|
@@ -677,15 +677,46 @@ domain nlq {
|
|
|
677
677
|
artifacts = emit_typescript(workspace, tmp_path / "out")
|
|
678
678
|
ir_art = next(a for a in artifacts if "NlqIr" in a.ref)
|
|
679
679
|
|
|
680
|
-
# Resolved named type uses
|
|
681
|
-
assert "
|
|
682
|
-
|
|
683
|
-
assert "
|
|
684
|
-
assert "NlqNlqTimeRange" in ir_art.content
|
|
680
|
+
# Resolved named type uses the exported short alias as the canonical name.
|
|
681
|
+
assert "timeRange: NlqTimeRange;" in ir_art.content
|
|
682
|
+
assert 'import type { NlqTimeRange } from "./nlq.NlqTimeRange.v0";' in ir_art.content
|
|
683
|
+
assert "NlqNlqTimeRangeV" not in ir_art.content
|
|
685
684
|
# EMIT003 must NOT be emitted when the type is successfully resolved
|
|
686
685
|
assert not any("EMIT003" in w for w in ir_art.warnings)
|
|
687
686
|
|
|
688
687
|
|
|
688
|
+
def test_emit_typescript_named_type_imports_after_docblock_and_uses_alias(tmp_path):
|
|
689
|
+
"""NamedType imports use the exported short alias after the metadata docblock."""
|
|
690
|
+
(tmp_path / "test.mdl").write_text(
|
|
691
|
+
"""
|
|
692
|
+
domain nlq {
|
|
693
|
+
owner: "test-team"
|
|
694
|
+
value NlqFilter {
|
|
695
|
+
field: string
|
|
696
|
+
value: string
|
|
697
|
+
}
|
|
698
|
+
value NlqIr {
|
|
699
|
+
filters: array<NlqFilter>
|
|
700
|
+
query: string
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
""",
|
|
704
|
+
encoding="utf-8",
|
|
705
|
+
)
|
|
706
|
+
workspace = load_workspace(tmp_path)
|
|
707
|
+
artifacts = emit_typescript(workspace, tmp_path / "out")
|
|
708
|
+
ir_art = next(a for a in artifacts if "NlqIr" in a.ref)
|
|
709
|
+
lines = ir_art.content.splitlines()
|
|
710
|
+
|
|
711
|
+
docblock_end = lines.index(" */")
|
|
712
|
+
import_index = lines.index('import type { NlqFilter } from "./nlq.NlqFilter.v0";')
|
|
713
|
+
interface_index = lines.index("export interface NlqNlqIrV0 {")
|
|
714
|
+
|
|
715
|
+
assert docblock_end < import_index < interface_index
|
|
716
|
+
assert "filters: NlqFilter[];" in ir_art.content
|
|
717
|
+
assert "NlqNlqFilterV" not in ir_art.content
|
|
718
|
+
|
|
719
|
+
|
|
689
720
|
def test_emit_typescript_named_type_unresolvable_still_warns(tmp_path):
|
|
690
721
|
"""NamedType not found in workspace still emits EMIT003 (issue #118)."""
|
|
691
722
|
(tmp_path / "test.mdl").write_text(
|