modelable 1.0.0__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.0 → modelable-1.0.2}/.gitignore +1 -0
- {modelable-1.0.0 → modelable-1.0.2}/PKG-INFO +1 -1
- {modelable-1.0.0 → modelable-1.0.2}/pyproject.toml +1 -1
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/commands/spec.py +36 -16
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/emitters/rust.py +241 -18
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/emitters/typescript.py +38 -6
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/grammar/modelable.lark +5 -1
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/parser/transformer.py +3 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/specs/tracking.py +39 -14
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_cli.py +2 -1
- modelable-1.0.2/tests/test_conformance_fixture.py +102 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_emit_rust.py +284 -4
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_emit_typescript.py +84 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_release_metadata.py +3 -1
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_repository_release.py +23 -2
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_spec_tracking.py +97 -1
- {modelable-1.0.0 → modelable-1.0.2}/uv.lock +1 -1
- {modelable-1.0.0 → modelable-1.0.2}/.python-version +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/LICENSE +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/README.md +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/__init__.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/__main__.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/_pydantic_py314_compat.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/cli.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/commands/__init__.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/commands/apicurio.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/commands/codegen.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/commands/common.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/commands/compile.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/commands/create.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/commands/diff.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/commands/graph.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/commands/llm.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/commands/lsp.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/commands/runtime.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/commands/scenario.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/commands/workspace.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/compat/__init__.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/compat/checker.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/compat/diff.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/compiler/__init__.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/compiler/compiler.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/compiler/workspace.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/diagnostics/__init__.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/diagnostics/model.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/emitters/__init__.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/emitters/base.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/emitters/csharp.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/emitters/dbt_yaml.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/emitters/diagnostics.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/emitters/fhir.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/emitters/fhir_validator.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/emitters/go.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/emitters/java.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/emitters/json_schema.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/emitters/markdown.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/emitters/odcs.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/emitters/openlineage.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/emitters/openmetadata.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/emitters/python.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/emitters/shapes.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/emitters/sql.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/emitters/targets.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/expressions/__init__.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/expressions/cel.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/governance/__init__.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/governance/checker.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/governance/por.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/grammar/__init__.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/graph/__init__.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/graph/export.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/llm/__init__.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/llm/chat.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/llm/config.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/llm/context.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/llm/engine.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/llm/importers.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/llm/provenance.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/llm/providers.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/llm/qa.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/llm/recommendations.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/llm/redaction.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/llm/render.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/llm/update_plan.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/llm/validation_help.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/lsp/__init__.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/lsp/__main__.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/lsp/code_actions.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/lsp/completion.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/lsp/definition.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/lsp/diagnostics.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/lsp/document_symbols.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/lsp/federation.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/lsp/folding.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/lsp/formatting.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/lsp/highlight.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/lsp/hover.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/lsp/inlay_hints.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/lsp/references.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/lsp/rename.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/lsp/semantic_tokens.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/lsp/server.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/lsp/workspace.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/lsp/workspace_symbols.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/parser/__init__.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/parser/ir.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/parser/parse.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/parser/wire.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/planner/__init__.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/planner/lineage.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/planner/planner.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/planner/plans.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/py.typed +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/registry/__init__.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/registry/apicurio.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/registry/base.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/registry/factory.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/registry/index.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/registry/local.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/registry/oci.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/registry/resolver.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/registry/schema.sql +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/registry/signature.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/release.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/runtime/__init__.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/runtime/adapter/__init__.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/runtime/adapter/base.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/runtime/adapter/postgres.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/specs/__init__.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/validation/__init__.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/src/modelable/validation/semantic.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/conftest.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/fixtures/auto_projection_complex.mdl +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/fixtures/billing_projection.mdl +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/fixtures/breaking_changes.mdl +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/fixtures/cel_error_cases.mdl +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/fixtures/customer.mdl +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/fixtures/federation_multi_registry/consumer.mdl +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/fixtures/federation_multi_registry/provider.mdl +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/fixtures/federation_multi_registry/workspace.mdl +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/fixtures/fhir_patient_profile.mdl +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/fixtures/governance_export_model.mdl +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/fixtures/materialized_projection_chain.mdl +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/fixtures/missing_header.mdl +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/fixtures/multi_domain_joins.mdl +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/fixtures/multi_language_target.mdl +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/fixtures/no_owner.mdl +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/fixtures/pii_governance.mdl +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/fixtures/projection_of_projection.mdl +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/fixtures/sql_and_dbt_targets.mdl +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/helpers.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_apicurio_registry.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_apicurio_testcontainers.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_auto_projection.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_cel_validation.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_cli_create.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_cli_inspect.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_codegen_docker_smoke.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_codegen_targets.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_compatibility.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_compiler.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_diagnostics.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_emit_csharp.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_emit_dbt_yaml.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_emit_fhir.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_emit_go.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_emit_java.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_emit_json_schema.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_emit_markdown.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_emit_odcs.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_emit_openlineage.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_emit_openmetadata.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_emit_python.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_emit_sql.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_fhir_validator.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_fixtures_regression.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_governance.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_grammar.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_graph_export.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_impact_analysis.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_lineage.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_llm_features.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_llm_provider_integration.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_lsp_code_actions.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_lsp_completion.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_lsp_completion_mirrors.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_lsp_definition.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_lsp_diagnostics.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_lsp_document_symbols.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_lsp_features.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_lsp_federation_diagnostics.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_lsp_folding.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_lsp_formatting.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_lsp_highlight.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_lsp_hover.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_lsp_inlay_hints.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_lsp_integration.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_lsp_references.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_lsp_rename.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_lsp_semantic_tokens.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_lsp_server.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_lsp_workspace.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_lsp_workspace_symbols.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_openmetadata_testcontainers.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_planner.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_por.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_postgres_adapter.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_py314_compat.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_registry.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_registry_index.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_release_workflow.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_resolver.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_samples.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_semantic.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_serialization_hints.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_transformer.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_validate_surface_detection.py +0 -0
- {modelable-1.0.0 → modelable-1.0.2}/tests/test_version_range_resolution.py +0 -0
- {modelable-1.0.0 → 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
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
|
+
import time
|
|
5
6
|
from dataclasses import asdict
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
@@ -70,25 +71,44 @@ def add(
|
|
|
70
71
|
@click.option("--path", "workspace_path", type=click.Path(exists=True, path_type=Path), default=Path("."))
|
|
71
72
|
@click.option("--json", "json_output", is_flag=True, help="Print machine-readable JSON.")
|
|
72
73
|
@click.option("--fail-on", default="", help="Comma-separated statuses that should return a non-zero exit code.")
|
|
73
|
-
|
|
74
|
+
@click.option("--poll-interval", default=None, type=float, help="Re-check every N seconds (useful in CI watch mode).")
|
|
75
|
+
@click.option("--max-polls", default=None, type=int, help="Stop after this many checks when --poll-interval is set.")
|
|
76
|
+
def status(
|
|
77
|
+
workspace_path: Path,
|
|
78
|
+
json_output: bool,
|
|
79
|
+
fail_on: str,
|
|
80
|
+
poll_interval: float | None,
|
|
81
|
+
max_polls: int | None,
|
|
82
|
+
) -> None:
|
|
74
83
|
"""Report drift status for tracked external specifications."""
|
|
75
84
|
token = os.getenv("MODELABLE_SPEC_TOKEN")
|
|
76
|
-
entries = select_specs(workspace_path, None)
|
|
77
|
-
evaluations = [evaluate_spec(workspace_path, entry, write=False, token=token) for entry in entries]
|
|
78
|
-
payload = {"specs": [evaluation.as_status_dict() for evaluation in evaluations]}
|
|
79
|
-
if json_output:
|
|
80
|
-
click.echo(json.dumps(payload, indent=2, sort_keys=True))
|
|
81
|
-
else:
|
|
82
|
-
if not evaluations:
|
|
83
|
-
console.print(f"[yellow]No tracked specs in {spec_config_path(workspace_path)}[/yellow]")
|
|
84
|
-
for evaluation in evaluations:
|
|
85
|
-
suffix = ""
|
|
86
|
-
if evaluation.change_kind:
|
|
87
|
-
suffix = f" ({evaluation.change_kind}, {evaluation.change_count} change(s))"
|
|
88
|
-
if evaluation.error:
|
|
89
|
-
suffix = f" ({evaluation.error})"
|
|
90
|
-
console.print(f"{evaluation.entry.id}: {evaluation.status}{suffix}")
|
|
91
85
|
fail_statuses = {item.strip() for item in fail_on.split(",") if item.strip()}
|
|
86
|
+
polls = 0
|
|
87
|
+
|
|
88
|
+
while True:
|
|
89
|
+
entries = select_specs(workspace_path, None)
|
|
90
|
+
evaluations = [evaluate_spec(workspace_path, entry, write=False, token=token) for entry in entries]
|
|
91
|
+
payload = {"specs": [evaluation.as_status_dict() for evaluation in evaluations]}
|
|
92
|
+
if json_output:
|
|
93
|
+
click.echo(json.dumps(payload, indent=2, sort_keys=True))
|
|
94
|
+
else:
|
|
95
|
+
if not evaluations:
|
|
96
|
+
console.print(f"[yellow]No tracked specs in {spec_config_path(workspace_path)}[/yellow]")
|
|
97
|
+
for evaluation in evaluations:
|
|
98
|
+
suffix = ""
|
|
99
|
+
if evaluation.change_kind:
|
|
100
|
+
suffix = f" ({evaluation.change_kind}, {evaluation.change_count} change(s))"
|
|
101
|
+
if evaluation.error:
|
|
102
|
+
suffix = f" ({evaluation.error})"
|
|
103
|
+
console.print(f"{evaluation.entry.id}: {evaluation.status}{suffix}")
|
|
104
|
+
|
|
105
|
+
polls += 1
|
|
106
|
+
if poll_interval is None:
|
|
107
|
+
break
|
|
108
|
+
if max_polls is not None and polls >= max_polls:
|
|
109
|
+
break
|
|
110
|
+
time.sleep(poll_interval)
|
|
111
|
+
|
|
92
112
|
if fail_statuses and any(evaluation.status in fail_statuses for evaluation in evaluations):
|
|
93
113
|
raise click.ClickException("tracked spec status matched --fail-on")
|
|
94
114
|
|
|
@@ -7,9 +7,18 @@ from pathlib import Path
|
|
|
7
7
|
|
|
8
8
|
from modelable.compiler.workspace import Workspace
|
|
9
9
|
from modelable.emitters.base import EmittedArtifact, compute_content_hash
|
|
10
|
-
from modelable.emitters.diagnostics import type_loss
|
|
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
|
|
|
@@ -22,15 +31,89 @@ class _FieldSpec:
|
|
|
22
31
|
serde_attrs: list[str] = dc_field(default_factory=list)
|
|
23
32
|
|
|
24
33
|
|
|
34
|
+
def _append_cross_enum_from_impls(
|
|
35
|
+
artifacts: list[EmittedArtifact],
|
|
36
|
+
enum_registry: dict[str, dict],
|
|
37
|
+
) -> list[EmittedArtifact]:
|
|
38
|
+
"""For each pair of enum types with identical variants in the same domain,
|
|
39
|
+
append From impl blocks into projection files without manual match arms.
|
|
40
|
+
"""
|
|
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]]] = {}
|
|
43
|
+
for art_id, info in enum_registry.items():
|
|
44
|
+
domain = info["domain"]
|
|
45
|
+
by_domain.setdefault(domain, []).append((art_id, info["module_name"], info["enums"], info["kind"]))
|
|
46
|
+
|
|
47
|
+
# Build: frozenset(raw_variants) -> [(artifact_id, module_name, enum_type_name)]
|
|
48
|
+
# Per domain — From impls only work within the same Rust module tree (super:: path).
|
|
49
|
+
extra: dict[str, list[str]] = {} # artifact_id -> lines to append
|
|
50
|
+
for _domain, entries in by_domain.items():
|
|
51
|
+
variant_map: dict[frozenset, list[tuple[str, str, str, str]]] = {}
|
|
52
|
+
for art_id, module_name, enums, kind in entries:
|
|
53
|
+
for enum_type_name, raw_variants in enums.items():
|
|
54
|
+
key = frozenset(raw_variants)
|
|
55
|
+
variant_map.setdefault(key, []).append((art_id, module_name, enum_type_name, kind))
|
|
56
|
+
|
|
57
|
+
for variant_set, enum_list in variant_map.items():
|
|
58
|
+
if len(enum_list) < 2:
|
|
59
|
+
continue
|
|
60
|
+
sorted_variants = sorted(variant_set)
|
|
61
|
+
# For each ordered pair (src → tgt): put From<src> for tgt in tgt's file.
|
|
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:
|
|
64
|
+
if src_art_id == tgt_art_id and src_enum == tgt_enum:
|
|
65
|
+
continue
|
|
66
|
+
if tgt_kind == "model":
|
|
67
|
+
continue
|
|
68
|
+
lines: list[str] = [
|
|
69
|
+
"",
|
|
70
|
+
f"use super::{src_module}::{src_enum};",
|
|
71
|
+
f"impl From<{src_enum}> for {tgt_enum} {{",
|
|
72
|
+
f" fn from(src: {src_enum}) -> Self {{",
|
|
73
|
+
" match src {",
|
|
74
|
+
]
|
|
75
|
+
for raw_v in sorted_variants:
|
|
76
|
+
member = _enum_member_name(raw_v)
|
|
77
|
+
lines.append(f" {src_enum}::{member} => {tgt_enum}::{member},")
|
|
78
|
+
lines += [" }", " }", "}"]
|
|
79
|
+
extra.setdefault(tgt_art_id, []).extend(lines)
|
|
80
|
+
|
|
81
|
+
if not extra:
|
|
82
|
+
return artifacts
|
|
83
|
+
|
|
84
|
+
result: list[EmittedArtifact] = []
|
|
85
|
+
for artifact in artifacts:
|
|
86
|
+
appendage = extra.get(artifact.artifact_id)
|
|
87
|
+
if appendage:
|
|
88
|
+
new_content = artifact.content.rstrip("\n") + "\n" + "\n".join(appendage) + "\n"
|
|
89
|
+
result.append(
|
|
90
|
+
EmittedArtifact(
|
|
91
|
+
target=artifact.target,
|
|
92
|
+
ref=artifact.ref,
|
|
93
|
+
artifact_id=artifact.artifact_id,
|
|
94
|
+
path=artifact.path,
|
|
95
|
+
content=new_content,
|
|
96
|
+
content_hash=compute_content_hash(new_content),
|
|
97
|
+
warnings=artifact.warnings,
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
else:
|
|
101
|
+
result.append(artifact)
|
|
102
|
+
return result
|
|
103
|
+
|
|
104
|
+
|
|
25
105
|
def emit_rust(workspace: Workspace, out_dir: Path) -> list[EmittedArtifact]:
|
|
26
106
|
"""Emit Rust source files for every model and projection version."""
|
|
27
107
|
postgres_sources = _adapter_bound_sources(workspace.mdl, "postgres")
|
|
28
108
|
clickhouse_sources = _adapter_bound_sources(workspace.mdl, "clickhouse")
|
|
109
|
+
enum_registry: dict[str, dict] = {}
|
|
29
110
|
artifacts: list[EmittedArtifact] = []
|
|
30
111
|
for domain in workspace.mdl.domains:
|
|
31
112
|
for model_name, versions in domain.models.items():
|
|
32
113
|
for version in versions:
|
|
33
|
-
artifacts.append(
|
|
114
|
+
artifacts.append(
|
|
115
|
+
_emit_model(domain, model_name, version, out_dir, enum_registry=enum_registry, mdl=workspace.mdl)
|
|
116
|
+
)
|
|
34
117
|
for projection_name, versions in domain.projections.items():
|
|
35
118
|
for version in versions:
|
|
36
119
|
source = version.source.model
|
|
@@ -43,9 +126,10 @@ def emit_rust(workspace: Workspace, out_dir: Path) -> list[EmittedArtifact]:
|
|
|
43
126
|
workspace.mdl,
|
|
44
127
|
sqlx_fromrow=source in postgres_sources,
|
|
45
128
|
clickhouse_row=source in clickhouse_sources,
|
|
129
|
+
enum_registry=enum_registry,
|
|
46
130
|
)
|
|
47
131
|
)
|
|
48
|
-
return artifacts
|
|
132
|
+
return _append_cross_enum_from_impls(artifacts, enum_registry)
|
|
49
133
|
|
|
50
134
|
|
|
51
135
|
def _adapter_bound_sources(mdl: MdlFile, adapter_type: str) -> set[str]:
|
|
@@ -86,18 +170,87 @@ def _stable_type_name(domain: str, name: str, version: int) -> str:
|
|
|
86
170
|
return f"{_pascalize(domain)}{_pascalize(name)}V{version}"
|
|
87
171
|
|
|
88
172
|
|
|
89
|
-
def
|
|
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
|
+
|
|
207
|
+
def _emit_model(
|
|
208
|
+
domain: DomainDef,
|
|
209
|
+
model_name: str,
|
|
210
|
+
version: ModelVersion,
|
|
211
|
+
out_dir: Path,
|
|
212
|
+
*,
|
|
213
|
+
enum_registry: dict[str, dict] | None = None,
|
|
214
|
+
mdl: MdlFile | None = None,
|
|
215
|
+
) -> EmittedArtifact:
|
|
90
216
|
artifact_id = _artifact_id(domain.name, model_name, version.version)
|
|
91
217
|
type_name = _stable_type_name(domain.name, model_name, version.version)
|
|
92
218
|
nested_definitions: dict[str, list[str]] = {}
|
|
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
|
+
|
|
93
227
|
field_specs = _field_specs_from_model_fields(
|
|
94
|
-
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,
|
|
95
234
|
)
|
|
235
|
+
if enum_registry is not None:
|
|
236
|
+
enum_registry[artifact_id] = {
|
|
237
|
+
"enums": local_enum_info,
|
|
238
|
+
"module_name": _snake_case(type_name),
|
|
239
|
+
"domain": domain.name,
|
|
240
|
+
"kind": "model",
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
warnings: list[str] = []
|
|
244
|
+
for field in version.fields:
|
|
245
|
+
if isinstance(field.type, NamedType) and field.type.name not in named_type_map:
|
|
246
|
+
warnings.append(missing_metadata(f"{domain.name}.{model_name}.{field.name}"))
|
|
96
247
|
|
|
97
248
|
needs_serde_with = _any_needs_serde_with(field_specs)
|
|
98
249
|
needs_uuid = _any_needs_uuid(field_specs)
|
|
99
250
|
needs_serde_json = _any_needs_serde_json(field_specs)
|
|
100
|
-
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
|
+
)
|
|
101
254
|
lines.extend(_render_struct_definition(type_name, field_specs))
|
|
102
255
|
lines.extend(_render_nested_definitions(nested_definitions))
|
|
103
256
|
|
|
@@ -109,7 +262,7 @@ def _emit_model(domain: DomainDef, model_name: str, version: ModelVersion, out_d
|
|
|
109
262
|
path=out_dir / _module_path(domain.name, type_name),
|
|
110
263
|
content=text,
|
|
111
264
|
content_hash=compute_content_hash(text),
|
|
112
|
-
warnings=
|
|
265
|
+
warnings=warnings,
|
|
113
266
|
)
|
|
114
267
|
|
|
115
268
|
|
|
@@ -122,10 +275,12 @@ def _emit_projection(
|
|
|
122
275
|
*,
|
|
123
276
|
sqlx_fromrow: bool = False,
|
|
124
277
|
clickhouse_row: bool = False,
|
|
278
|
+
enum_registry: dict[str, dict] | None = None,
|
|
125
279
|
) -> EmittedArtifact:
|
|
126
280
|
artifact_id = _artifact_id(domain.name, projection_name, version.version)
|
|
127
281
|
type_name = _stable_type_name(domain.name, projection_name, version.version)
|
|
128
282
|
nested_definitions: dict[str, list[str]] = {}
|
|
283
|
+
local_enum_info: dict[str, list[str]] = {}
|
|
129
284
|
|
|
130
285
|
field_specs: list[_FieldSpec] = []
|
|
131
286
|
warnings: list[str] = []
|
|
@@ -136,16 +291,24 @@ def _emit_projection(
|
|
|
136
291
|
field_specs.append(_FieldSpec(index=index, name=field.name, annotation="String", optional=False))
|
|
137
292
|
continue
|
|
138
293
|
wire = _resolve_merged_projection_wire(field, version, mdl)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
+
)
|
|
147
308
|
optional = field_shape.optional or field_shape.nullable
|
|
148
309
|
serde_attrs = _serde_attrs_for_field(wire, field_shape, clickhouse=clickhouse_row)
|
|
310
|
+
if field_shape.optional and not clickhouse_row:
|
|
311
|
+
serde_attrs = ['#[serde(skip_serializing_if = "Option::is_none")]', *serde_attrs]
|
|
149
312
|
field_specs.append(
|
|
150
313
|
_FieldSpec(index=index, name=field.name, annotation=annotation, optional=optional, serde_attrs=serde_attrs)
|
|
151
314
|
)
|
|
@@ -172,7 +335,19 @@ def _emit_projection(
|
|
|
172
335
|
_render_struct_definition(type_name, field_specs, extra_derives=extra_derives, storage_gated=storage_gated)
|
|
173
336
|
)
|
|
174
337
|
lines.extend(_render_nested_definitions(nested_definitions))
|
|
175
|
-
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
|
+
)
|
|
343
|
+
|
|
344
|
+
if enum_registry is not None:
|
|
345
|
+
enum_registry[artifact_id] = {
|
|
346
|
+
"enums": local_enum_info,
|
|
347
|
+
"module_name": _snake_case(type_name),
|
|
348
|
+
"domain": domain.name,
|
|
349
|
+
"kind": "projection",
|
|
350
|
+
}
|
|
176
351
|
|
|
177
352
|
text = "\n".join(lines) + "\n"
|
|
178
353
|
return EmittedArtifact(
|
|
@@ -218,6 +393,7 @@ def _emit_from_impl(
|
|
|
218
393
|
mdl: MdlFile,
|
|
219
394
|
*,
|
|
220
395
|
storage_gated: bool = False,
|
|
396
|
+
clickhouse_row: bool = False,
|
|
221
397
|
) -> list[str]:
|
|
222
398
|
"""Emit impl From<SourceModel> for Projection.
|
|
223
399
|
|
|
@@ -250,6 +426,16 @@ def _emit_from_impl(
|
|
|
250
426
|
if storage_gated:
|
|
251
427
|
lines.append('#[cfg(feature = "storage")]')
|
|
252
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};")
|
|
253
439
|
if storage_gated:
|
|
254
440
|
lines.append('#[cfg(feature = "storage")]')
|
|
255
441
|
lines.append(f"impl From<{src_type_name}> for {proj_type_name} {{")
|
|
@@ -261,7 +447,15 @@ def _emit_from_impl(
|
|
|
261
447
|
if isinstance(proj_field.mapping, DirectMapping):
|
|
262
448
|
src_rust_name = _field_name(proj_field.mapping.source_field)
|
|
263
449
|
field_shape = _resolve_projection_field_shape(proj_field, version, mdl)
|
|
264
|
-
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):
|
|
265
459
|
lines.append(
|
|
266
460
|
f" {rust_name}: serde_json::to_string(&src.{src_rust_name}).unwrap_or_default(),"
|
|
267
461
|
)
|
|
@@ -333,6 +527,7 @@ def _header_lines(
|
|
|
333
527
|
clickhouse: bool = False,
|
|
334
528
|
uuid: bool = False,
|
|
335
529
|
serde_json: bool = False,
|
|
530
|
+
extra_uses: list[str] | None = None,
|
|
336
531
|
) -> list[str]:
|
|
337
532
|
lines = [
|
|
338
533
|
"// @generated by Modelable",
|
|
@@ -349,6 +544,9 @@ def _header_lines(
|
|
|
349
544
|
lines.insert(1, "// requires: uuid (https://docs.rs/uuid)")
|
|
350
545
|
if serde_json:
|
|
351
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
|
|
352
550
|
return lines
|
|
353
551
|
|
|
354
552
|
|
|
@@ -437,6 +635,8 @@ def _field_specs_from_model_fields(
|
|
|
437
635
|
owner_type: str,
|
|
438
636
|
path: list[str],
|
|
439
637
|
definitions: dict[str, list[str]],
|
|
638
|
+
enum_info: dict[str, list[str]] | None = None,
|
|
639
|
+
named_type_map: dict[str, str] | None = None,
|
|
440
640
|
) -> list[_FieldSpec]:
|
|
441
641
|
specs: list[_FieldSpec] = []
|
|
442
642
|
for index, field in enumerate(fields):
|
|
@@ -448,6 +648,8 @@ def _field_specs_from_model_fields(
|
|
|
448
648
|
path=[*path, field.name],
|
|
449
649
|
definitions=definitions,
|
|
450
650
|
rust_hint=wire.get("rust"),
|
|
651
|
+
enum_info=enum_info,
|
|
652
|
+
named_type_map=named_type_map,
|
|
451
653
|
)
|
|
452
654
|
is_optional = shape.optional or shape.nullable
|
|
453
655
|
serde_attrs = _serde_attrs_for_field(wire, shape)
|
|
@@ -462,6 +664,10 @@ def _field_specs_from_model_fields(
|
|
|
462
664
|
definitions=definitions,
|
|
463
665
|
rust_hint=wire.get("rust"),
|
|
464
666
|
)
|
|
667
|
+
elif shape.optional:
|
|
668
|
+
# Omittable field: skip during serialization when None.
|
|
669
|
+
# Nullable-only fields must always be serialized (as null), so no skip attr.
|
|
670
|
+
serde_attrs = ['#[serde(skip_serializing_if = "Option::is_none")]', *serde_attrs]
|
|
465
671
|
specs.append(
|
|
466
672
|
_FieldSpec(
|
|
467
673
|
index=index, name=field.name, annotation=annotation, optional=is_optional, serde_attrs=serde_attrs
|
|
@@ -476,6 +682,7 @@ def _field_specs_from_object_fields(
|
|
|
476
682
|
owner_type: str,
|
|
477
683
|
path: list[str],
|
|
478
684
|
definitions: dict[str, list[str]],
|
|
685
|
+
enum_info: dict[str, list[str]] | None = None,
|
|
479
686
|
) -> list[_FieldSpec]:
|
|
480
687
|
specs: list[_FieldSpec] = []
|
|
481
688
|
for index, field in enumerate(fields):
|
|
@@ -486,6 +693,7 @@ def _field_specs_from_object_fields(
|
|
|
486
693
|
path=[*path, field.name],
|
|
487
694
|
definitions=definitions,
|
|
488
695
|
rust_hint=wire.get("rust"),
|
|
696
|
+
enum_info=enum_info,
|
|
489
697
|
)
|
|
490
698
|
default_none = field.optional or field.shape.optional or field.shape.nullable
|
|
491
699
|
serde_attrs = _serde_attrs_for_field(wire, field.shape)
|
|
@@ -505,6 +713,8 @@ def _shape_annotation(
|
|
|
505
713
|
definitions: dict[str, list[str]],
|
|
506
714
|
rust_hint=None,
|
|
507
715
|
clickhouse_hint=None,
|
|
716
|
+
enum_info: dict[str, list[str]] | None = None,
|
|
717
|
+
named_type_map: dict[str, str] | None = None,
|
|
508
718
|
) -> str:
|
|
509
719
|
base = _shape_base_annotation(
|
|
510
720
|
shape,
|
|
@@ -513,6 +723,8 @@ def _shape_annotation(
|
|
|
513
723
|
definitions=definitions,
|
|
514
724
|
rust_hint=rust_hint,
|
|
515
725
|
clickhouse_hint=clickhouse_hint,
|
|
726
|
+
enum_info=enum_info,
|
|
727
|
+
named_type_map=named_type_map,
|
|
516
728
|
)
|
|
517
729
|
if shape.optional or shape.nullable:
|
|
518
730
|
return f"Option<{base}>"
|
|
@@ -527,6 +739,8 @@ def _shape_base_annotation(
|
|
|
527
739
|
definitions: dict[str, list[str]],
|
|
528
740
|
rust_hint=None,
|
|
529
741
|
clickhouse_hint=None,
|
|
742
|
+
enum_info: dict[str, list[str]] | None = None,
|
|
743
|
+
named_type_map: dict[str, str] | None = None,
|
|
530
744
|
) -> str:
|
|
531
745
|
clickhouse_string = clickhouse_hint is not None and getattr(clickhouse_hint, "encoding", None) == "string"
|
|
532
746
|
if shape.kind == "primitive":
|
|
@@ -545,6 +759,8 @@ def _shape_base_annotation(
|
|
|
545
759
|
path=[*path, "Item"],
|
|
546
760
|
definitions=definitions,
|
|
547
761
|
rust_hint=rust_hint,
|
|
762
|
+
enum_info=enum_info,
|
|
763
|
+
named_type_map=named_type_map,
|
|
548
764
|
)
|
|
549
765
|
return f"Vec<{element_type}>"
|
|
550
766
|
if shape.kind == "map":
|
|
@@ -556,6 +772,8 @@ def _shape_base_annotation(
|
|
|
556
772
|
owner_type=owner_type,
|
|
557
773
|
path=[*path, "Value"],
|
|
558
774
|
definitions=definitions,
|
|
775
|
+
enum_info=enum_info,
|
|
776
|
+
named_type_map=named_type_map,
|
|
559
777
|
)
|
|
560
778
|
return f"HashMap<String, {value_type}>"
|
|
561
779
|
if shape.kind == "ref":
|
|
@@ -564,8 +782,12 @@ def _shape_base_annotation(
|
|
|
564
782
|
enum_type_name = _nested_type_name(owner_type, path)
|
|
565
783
|
if enum_type_name not in definitions:
|
|
566
784
|
definitions[enum_type_name] = _render_enum_definition(enum_type_name, list(shape.enum_values))
|
|
785
|
+
if enum_info is not None and enum_type_name not in enum_info:
|
|
786
|
+
enum_info[enum_type_name] = list(shape.enum_values)
|
|
567
787
|
return enum_type_name
|
|
568
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]
|
|
569
791
|
return _pascalize(shape.ref or "Named")
|
|
570
792
|
if shape.kind == "object":
|
|
571
793
|
type_name = _nested_type_name(owner_type, path)
|
|
@@ -577,6 +799,7 @@ def _shape_base_annotation(
|
|
|
577
799
|
owner_type=owner_type,
|
|
578
800
|
path=path,
|
|
579
801
|
definitions=definitions,
|
|
802
|
+
enum_info=enum_info,
|
|
580
803
|
),
|
|
581
804
|
)
|
|
582
805
|
return type_name
|
|
@@ -92,15 +92,39 @@ def _collect_ref_imports(field_type, mdl, resolved_refs: dict[str, str]) -> None
|
|
|
92
92
|
_collect_ref_imports(f.type, mdl, resolved_refs)
|
|
93
93
|
|
|
94
94
|
|
|
95
|
+
def _collect_named_imports(field_type, mdl, named_imports: dict[str, tuple[str, str]]) -> None:
|
|
96
|
+
"""Recursively resolve NamedType refs to (exported_alias, artifact_id) in named_imports."""
|
|
97
|
+
if isinstance(field_type, NamedType):
|
|
98
|
+
name = field_type.name
|
|
99
|
+
if name not in named_imports and mdl is not None:
|
|
100
|
+
for domain in mdl.domains:
|
|
101
|
+
if name in domain.models:
|
|
102
|
+
versions = domain.models[name]
|
|
103
|
+
if versions:
|
|
104
|
+
latest = max(versions, key=lambda v: v.version)
|
|
105
|
+
aid = _artifact_id(domain.name, name, latest.version)
|
|
106
|
+
named_imports[name] = (name, aid)
|
|
107
|
+
return
|
|
108
|
+
elif isinstance(field_type, ArrayType):
|
|
109
|
+
_collect_named_imports(field_type.item, mdl, named_imports)
|
|
110
|
+
elif isinstance(field_type, MapType):
|
|
111
|
+
_collect_named_imports(field_type.value, mdl, named_imports)
|
|
112
|
+
elif isinstance(field_type, ObjectType):
|
|
113
|
+
for f in field_type.fields:
|
|
114
|
+
_collect_named_imports(f.type, mdl, named_imports)
|
|
115
|
+
|
|
116
|
+
|
|
95
117
|
def _emit_model(domain: DomainDef, model_name: str, version: ModelVersion, out_dir: Path, mdl=None) -> EmittedArtifact:
|
|
96
118
|
artifact_id = _artifact_id(domain.name, model_name, version.version)
|
|
97
119
|
interface_name = _stable_interface_name(domain.name, model_name, version.version)
|
|
98
120
|
|
|
99
121
|
# Resolve ref<X> fields to stable interface names; collect imports.
|
|
100
122
|
resolved_refs: dict[str, str] = {} # ref target → stable interface name
|
|
123
|
+
named_imports: dict[str, tuple[str, str]] = {} # bare name → (stable iface name, artifact_id)
|
|
101
124
|
if mdl is not None:
|
|
102
125
|
for field in version.fields:
|
|
103
126
|
_collect_ref_imports(field.type, mdl, resolved_refs)
|
|
127
|
+
_collect_named_imports(field.type, mdl, named_imports)
|
|
104
128
|
|
|
105
129
|
declaration_json_wire = version.wire_targets().get("json")
|
|
106
130
|
field_case = declaration_json_wire.field_case if declaration_json_wire is not None else None
|
|
@@ -109,6 +133,9 @@ def _emit_model(domain: DomainDef, model_name: str, version: ModelVersion, out_d
|
|
|
109
133
|
for iface in sorted(set(resolved_refs.values())):
|
|
110
134
|
iface_id = _iface_to_artifact_id(iface)
|
|
111
135
|
import_lines.append(f'import type {{ {iface} }} from "./{iface_id}";')
|
|
136
|
+
for name in sorted(named_imports):
|
|
137
|
+
iface, aid = named_imports[name]
|
|
138
|
+
import_lines.append(f'import type {{ {iface} }} from "./{aid}";')
|
|
112
139
|
|
|
113
140
|
meta_lines = _metadata_lines(
|
|
114
141
|
_domain_metadata_entries(
|
|
@@ -123,16 +150,16 @@ def _emit_model(domain: DomainDef, model_name: str, version: ModelVersion, out_d
|
|
|
123
150
|
body_lines.append(f"export interface {interface_name} {{")
|
|
124
151
|
warnings: list[str] = []
|
|
125
152
|
for field in version.fields:
|
|
126
|
-
if isinstance(field.type, NamedType):
|
|
153
|
+
if isinstance(field.type, NamedType) and field.type.name not in named_imports:
|
|
127
154
|
warnings.append(missing_metadata(f"{domain.name}.{model_name}.{field.name}"))
|
|
128
155
|
field_name = _apply_case(field.name, field_case) if field_case else field.name
|
|
129
156
|
body_lines.append(
|
|
130
|
-
f" {field_name}{'?' if field.optional else ''}: {_type_to_ts(field.type, wire_targets=field.wire_targets(), resolved_refs=resolved_refs)};"
|
|
157
|
+
f" {field_name}{'?' if field.optional else ''}: {_type_to_ts(field.type, wire_targets=field.wire_targets(), resolved_refs=resolved_refs, named_imports=named_imports)};"
|
|
131
158
|
)
|
|
132
159
|
body_lines.append("}")
|
|
133
160
|
body_lines.append(f"export type {model_name} = {interface_name};")
|
|
134
161
|
|
|
135
|
-
all_lines =
|
|
162
|
+
all_lines = meta_lines + ([*import_lines, ""] if import_lines else []) + body_lines
|
|
136
163
|
content = "\n".join(all_lines) + "\n"
|
|
137
164
|
return EmittedArtifact(
|
|
138
165
|
target="typescript",
|
|
@@ -286,6 +313,7 @@ def _type_to_ts(
|
|
|
286
313
|
*,
|
|
287
314
|
wire_targets: dict[str, object] | None = None,
|
|
288
315
|
resolved_refs: dict[str, str] | None = None,
|
|
316
|
+
named_imports: dict[str, tuple[str, str]] | None = None,
|
|
289
317
|
) -> str:
|
|
290
318
|
json_wire = None
|
|
291
319
|
if wire_targets is not None:
|
|
@@ -314,12 +342,14 @@ def _type_to_ts(
|
|
|
314
342
|
if isinstance(field_type, DecimalType):
|
|
315
343
|
return "string"
|
|
316
344
|
if isinstance(field_type, ArrayType):
|
|
317
|
-
item_ts = _type_to_ts(field_type.item)
|
|
345
|
+
item_ts = _type_to_ts(field_type.item, resolved_refs=resolved_refs, named_imports=named_imports)
|
|
318
346
|
if isinstance(field_type.item, EnumType):
|
|
319
347
|
return f"({item_ts})[]"
|
|
320
348
|
return f"{item_ts}[]"
|
|
321
349
|
if isinstance(field_type, MapType):
|
|
322
|
-
return
|
|
350
|
+
return (
|
|
351
|
+
f"Record<string, {_type_to_ts(field_type.value, resolved_refs=resolved_refs, named_imports=named_imports)}>"
|
|
352
|
+
)
|
|
323
353
|
if isinstance(field_type, RefType):
|
|
324
354
|
if resolved_refs and field_type.target in resolved_refs:
|
|
325
355
|
return resolved_refs[field_type.target]
|
|
@@ -339,11 +369,13 @@ def _type_to_ts(
|
|
|
339
369
|
return values or "string"
|
|
340
370
|
if isinstance(field_type, ObjectType):
|
|
341
371
|
inner = "; ".join(
|
|
342
|
-
f"{field.name}{'?' if field.optional else ''}: {_type_to_ts(field.type, wire_targets=field.wire_targets(), resolved_refs=resolved_refs)}"
|
|
372
|
+
f"{field.name}{'?' if field.optional else ''}: {_type_to_ts(field.type, wire_targets=field.wire_targets(), resolved_refs=resolved_refs, named_imports=named_imports)}"
|
|
343
373
|
for field in field_type.fields
|
|
344
374
|
)
|
|
345
375
|
return f"{{ {inner} }}"
|
|
346
376
|
if isinstance(field_type, NamedType):
|
|
377
|
+
if named_imports and field_type.name in named_imports:
|
|
378
|
+
return named_imports[field_type.name][0]
|
|
347
379
|
return field_type.name
|
|
348
380
|
if field_type is None:
|
|
349
381
|
return "unknown"
|
|
@@ -96,7 +96,11 @@ primitive_type: "string" -> pt_string
|
|
|
96
96
|
| "json" -> pt_json
|
|
97
97
|
|
|
98
98
|
decimal_type: "decimal" "(" INT "," INT ")"
|
|
99
|
-
enum_type: "enum" "("
|
|
99
|
+
enum_type: "enum" "(" enum_member ("," enum_member)* ")"
|
|
100
|
+
enum_member: IDENT
|
|
101
|
+
| NUMERIC_IDENT
|
|
102
|
+
|
|
103
|
+
NUMERIC_IDENT: /[0-9][A-Za-z0-9_-]*/
|
|
100
104
|
array_type: "array" "<" type_expr ">"
|
|
101
105
|
map_type: "map" "<" type_expr "," type_expr ">"
|
|
102
106
|
ref_type: "ref" "<" dotted_ref ">"
|