From 737cc504dc5071deb4448a26e2a7a03f1879009a Mon Sep 17 00:00:00 2001 From: Aviator 5 Date: Mon, 29 Jun 2026 17:15:29 +0300 Subject: [PATCH] chore(deps): migrate to gts-rust v0.11.0 - Bump workspace GTS crates and refresh lockfiles for the v0.11.0 release. - Route GTS literals through toolkit-gts helpers and gts_id! validation. - Update canonical error, permission, scanner, and lint integrations for the new GTS APIs. Signed-off-by: Aviator 5 --- Cargo.lock | 61 +++- Cargo.toml | 9 +- .../03_clienthub_and_plugins.md | 17 +- .../users-info/users-info-sdk/Cargo.toml | 1 + .../users-info/users-info-sdk/src/gts.rs | 7 +- .../users-info/src/api/rest/error.rs | 6 +- .../users-info/src/api/rest/routes/mod.rs | 4 +- gears/chat-engine/chat-engine/Cargo.toml | 1 + .../chat-engine/src/api/rest/error.rs | 8 +- .../chat-engine/src/api/rest/routes/mod.rs | 9 +- .../src/infra/webhook_compat_tests.rs | 6 +- gears/credstore/credstore-sdk/src/gts.rs | 2 +- gears/credstore/credstore/Cargo.toml | 1 + .../credstore/src/domain/error_tests.rs | 17 +- gears/file-parser/src/api/rest/error.rs | 2 +- gears/file-parser/src/api/rest/routes.rs | 7 +- .../file-storage/file-storage-sdk/Cargo.toml | 1 + .../file-storage/file-storage-sdk/src/gts.rs | 4 +- .../file-storage-sdk/src/gts_tests.rs | 5 +- gears/file-storage/file-storage/Cargo.toml | 2 + .../file-storage/src/api/rest/error.rs | 2 +- .../file-storage/src/api/rest/routes.rs | 3 +- .../src/domain/multipart_service.rs | 3 +- .../file-storage/src/domain/service/mod.rs | 5 +- .../src/infra/external_clients.rs | 2 +- .../file-storage/tests/audit_test.rs | 3 +- .../file-storage/tests/cleanup_test.rs | 3 +- .../file-storage/tests/enforce_test.rs | 3 +- .../file-storage/tests/migration_test.rs | 3 +- .../file-storage/tests/multipart_test.rs | 3 +- .../file-storage/tests/ownership_test.rs | 3 +- .../file-storage/tests/service_test.rs | 3 +- gears/mini-chat/mini-chat-sdk/src/gts.rs | 16 +- gears/mini-chat/mini-chat-sdk/src/lib.rs | 1 + .../mini-chat/mini-chat/src/api/rest/error.rs | 37 +- .../mini-chat/src/api/rest/routes/mod.rs | 3 +- gears/mini-chat/mini-chat/src/config.rs | 13 +- .../domain/service/attachment_service_test.rs | 2 +- .../mini-chat/src/domain/service/mod.rs | 11 +- .../mini-chat/src/gts/permissions.rs | 93 ++--- .../llm/providers/openai_responses_tests.rs | 2 +- .../mini-chat/src/infra/oagw_provisioning.rs | 9 +- .../model-registry-sdk/Cargo.toml | 2 +- .../model-registry-sdk/src/models/entity.rs | 7 +- .../model-registry-sdk/src/models/info.rs | 13 +- .../src/models/providers/anthropic.rs | 10 +- .../src/models/providers/mod.rs | 6 +- .../src/models/providers/openai.rs | 10 +- .../model-registry-sdk/src/models/request.rs | 3 +- .../simple-user-settings/Cargo.toml | 3 + .../src/api/rest/error.rs | 2 +- .../src/api/rest/error_test.rs | 7 +- .../src/api/rest/routes.rs | 9 +- .../account-management-sdk/Cargo.toml | 1 - .../account-management-sdk/src/error_tests.rs | 12 +- .../account-management-sdk/src/gts.rs | 51 ++- .../account-management-sdk/src/idp.rs | 2 +- .../account-management-sdk/src/idp_tests.rs | 7 +- .../src/idp_user_tests.rs | 13 +- .../account-management-sdk/src/metadata.rs | 2 +- .../src/metadata_tests.rs | 4 +- .../account-management-sdk/src/tenant.rs | 2 +- .../src/api/rest/dto_tests.rs | 70 ++-- .../src/api/rest/handlers/metadata_tests.rs | 13 +- .../src/api/rest/handlers/tenants_tests.rs | 5 +- .../src/domain/bootstrap/config_tests.rs | 31 +- .../src/domain/bootstrap/service.rs | 16 +- .../src/domain/bootstrap/service_tests.rs | 21 +- .../src/domain/conversion/service.rs | 2 +- .../src/domain/error_tests.rs | 5 +- .../src/domain/gts_validation.rs | 5 +- .../src/domain/metadata/registry.rs | 8 +- .../src/domain/metadata/service.rs | 4 +- .../src/domain/metadata/service_tests.rs | 15 +- .../src/domain/metadata/type_id.rs | 27 +- .../src/domain/metadata/type_id_tests.rs | 24 +- .../src/domain/tenant/service/mod.rs | 7 +- .../domain/tenant/service/service_tests.rs | 22 +- .../src/domain/user/service.rs | 5 +- .../src/domain/user/service_tests.rs | 13 +- .../src/domain/user_groups/cascade_tests.rs | 2 +- .../domain/user_groups/registration_tests.rs | 7 +- .../account-management/src/gear_tests.rs | 12 +- .../account-management/src/gts/permissions.rs | 76 ++-- .../src/infra/idp/lazy_tests.rs | 3 +- .../src/infra/idp/noop_tests.rs | 5 +- .../src/infra/rg/checker_tests.rs | 3 +- .../src/infra/sdk_error_mapping.rs | 8 +- .../src/infra/sdk_error_mapping_tests.rs | 5 +- .../src/infra/storage/repo_impl/reads.rs | 16 +- .../src/infra/types_registry/checker.rs | 3 +- .../src/infra/types_registry/checker_tests.rs | 83 ++--- .../metadata_schema_registry.rs | 11 +- .../account-management/src/tr_plugin/tests.rs | 3 +- .../tests/api_conversions_test.rs | 8 +- .../tests/api_status_mapping_test.rs | 9 +- .../tests/api_tenants_test.rs | 3 +- .../account-management/tests/common/mod.rs | 14 +- .../tests/metadata_integration.rs | 9 +- .../tests/metadata_integration_pg.rs | 7 +- .../plugins/static-idp-plugin/Cargo.toml | 1 + .../src/domain/client_tests.rs | 3 +- gears/system/api-gateway/Cargo.toml | 1 + .../api-gateway/src/middleware/errors.rs | 4 +- .../src/middleware/license_validation.rs | 3 +- .../src/middleware/scope_enforcement.rs | 5 +- .../api-gateway/tests/auth_middleware.rs | 16 +- .../api-gateway/tests/auth_middleware.rs.orig | 6 +- .../api-gateway/tests/license_middleware.rs | 7 +- .../tests/license_middleware.rs.orig | 2 +- .../tests/mime_validation_integration.rs | 3 +- .../api-gateway/tests/rate_limit_tests.rs | 5 +- .../authn-resolver-sdk/src/gts.rs | 2 +- .../plugins/oidc-authn-plugin/Cargo.toml | 1 + .../oidc-authn-plugin/benches/s2s_exchange.rs | 3 +- .../plugins/oidc-authn-plugin/src/config.rs | 114 +++--- .../src/domain/authenticate_tests.rs | 3 +- .../oidc-authn-plugin/tests/common/mod.rs | 3 +- .../plugins/static-authn-plugin/src/config.rs | 2 +- .../authz-resolver-sdk/src/gts.rs | 2 +- .../authz-resolver-sdk/src/lib.rs | 2 +- .../authz-resolver-sdk/src/pep/enforcer.rs | 4 +- .../src/pep/enforcer_tests.rs | 15 +- .../plugins/static-authz-plugin/Cargo.toml | 1 + .../src/domain/service_tests.rs | 5 +- .../plugins/tr-authz-plugin/Cargo.toml | 1 + .../src/domain/service_tests.rs | 7 +- gears/system/cluster/cluster-sdk/src/gts.rs | 2 +- .../nodes-registry-sdk/Cargo.toml | 1 + .../nodes-registry-sdk/src/error_tests.rs | 4 +- .../nodes-registry-sdk/src/gts.rs | 3 +- .../nodes-registry/nodes-registry/Cargo.toml | 3 + .../nodes-registry/src/api/rest/error.rs | 2 +- .../nodes-registry/tests/error_tests.rs | 17 +- gears/system/oagw/oagw-sdk/Cargo.toml | 1 + gears/system/oagw/oagw-sdk/src/error.rs | 19 +- gears/system/oagw/oagw-sdk/src/gts.rs | 28 +- gears/system/oagw/oagw-sdk/src/lib.rs | 4 + gears/system/oagw/oagw/Cargo.toml | 1 + gears/system/oagw/oagw/src/api/rest/error.rs | 39 ++- .../oagw/oagw/src/api/rest/extractors.rs | 5 +- .../oagw/oagw/src/api/rest/routes/mod.rs | 5 +- .../oagw/oagw/src/domain/gts_helpers.rs | 75 ++-- .../src/domain/services/management/bind.rs | 3 +- .../src/domain/services/management/tests.rs | 21 +- .../oagw/oagw/src/domain/type_catalog.rs | 14 +- .../oagw/oagw/src/infra/plugin/registry.rs | 13 +- gears/system/oagw/oagw/src/infra/proxy/mod.rs | 7 +- .../oagw/src/infra/storage/upstream_repo.rs | 3 +- .../oagw/oagw/src/infra/type_provisioning.rs | 41 ++- .../system/oagw/oagw/tests/e2e_http2_test.rs | 9 +- .../system/oagw/oagw/tests/e2e_smoke_test.rs | 38 +- .../oagw/oagw/tests/management_api_test.rs | 48 ++- .../oagw/oagw/tests/proxy_integration.rs | 109 +++--- .../docs/features/0001-sdk-gear-foundation.md | 6 +- .../resource-group-sdk/src/api.rs | 2 +- .../resource-group-sdk/src/error_tests.rs | 4 +- .../resource-group-sdk/src/gts.rs | 10 +- .../resource-group-sdk/src/lib.rs | 2 +- .../resource-group-sdk/src/models.rs | 17 +- .../resource-group-sdk/src/models_tests.rs | 75 ++-- .../resource-group/resource-group/Cargo.toml | 1 + .../resource-group/src/api/rest/dto_tests.rs | 64 ++-- .../resource-group/src/api/rest/error.rs | 2 +- .../src/domain/group_service.rs | 4 +- .../src/domain/membership_service.rs | 4 +- .../resource-group/src/domain/validation.rs | 13 +- .../src/infra/storage/group_repo.rs | 2 +- .../resource-group/tests/api_rest_test.rs | 181 +++------- .../tests/authz_integration_test.rs | 3 +- .../resource-group/tests/common/mod.rs | 7 +- .../resource-group/tests/domain_unit_test.rs | 29 +- .../tests/group_service_test.rs | 14 +- .../tests/membership_service_test.rs | 17 +- .../resource-group/tests/read_service_test.rs | 3 +- .../resource-group/tests/seeding_test.rs | 3 +- .../tests/tenant_filtering_db_test.rs | 5 +- .../resource-group/tests/type_service_test.rs | 3 +- .../tenant-resolver-sdk/src/gts.rs | 2 +- .../types-registry-sdk/Cargo.toml | 1 + .../types-registry-sdk/src/api.rs | 2 +- .../types-registry-sdk/src/error_tests.rs | 43 ++- .../types-registry-sdk/src/gts.rs | 5 +- .../types-registry-sdk/src/lib.rs | 4 +- .../types-registry-sdk/src/models.rs | 38 +- .../types-registry-sdk/src/models_tests.rs | 73 ++-- .../types-registry-sdk/src/testing.rs | 6 +- .../types-registry/src/api/rest/dto.rs | 63 ++-- .../types-registry/src/api/rest/error.rs | 19 +- .../types-registry/src/api/rest/handlers.rs | 19 +- .../types-registry/src/api/rest/routes.rs | 6 +- .../types-registry/src/domain/error.rs | 25 +- .../src/domain/local_client_tests.rs | 224 ++++++------ .../types-registry/src/domain/model.rs | 6 +- .../types-registry/src/domain/service.rs | 32 +- .../src/infra/cache/cache_tests.rs | 131 +++---- .../src/infra/storage/debug_diagnostics.rs | 84 +++-- .../src/infra/storage/in_memory_repo.rs | 105 +++--- .../types-registry/tests/edge_cases_tests.rs | 27 +- .../types-registry/tests/query_tests.rs | 38 +- .../types-registry/tests/ready_mode_tests.rs | 68 ++-- .../tests/registration_tests.rs | 28 +- .../tests/rg_gts_type_system_tests.rs | 136 ++++---- .../tests/type_instance_tests.rs | 52 +-- .../usage-collector-sdk/src/gts.rs | 7 +- .../usage-collector-sdk/src/models.rs | 10 +- .../usage-collector-sdk/src/models_tests.rs | 55 +-- .../usage-collector/Cargo.toml | 1 + .../usage-collector/src/api/rest/dto_tests.rs | 6 +- .../api/rest/handlers/usage_records_tests.rs | 30 +- .../api/rest/handlers/usage_types_tests.rs | 6 +- .../usage-collector/src/domain/authz.rs | 7 +- .../usage-collector/src/domain/authz_tests.rs | 3 +- .../usage-collector/src/domain/error_tests.rs | 3 +- .../src/domain/local_client_tests.rs | 3 +- .../src/domain/service_tests.rs | 43 ++- .../src/domain/validation_tests.rs | 5 +- .../usage-collector/src/gts/permissions.rs | 36 +- .../src/infra/sdk_error_mapping.rs | 4 +- .../src/infra/sdk_error_mapping_tests.rs | 55 +-- guidelines/GTS.md | 179 ++++++++-- .../toolkit-canonical-errors-macro/Cargo.toml | 4 + .../toolkit-canonical-errors-macro/src/lib.rs | 127 +++---- libs/toolkit-canonical-errors/Cargo.toml | 2 +- libs/toolkit-canonical-errors/src/error.rs | 43 ++- libs/toolkit-canonical-errors/src/lib.rs | 6 + libs/toolkit-canonical-errors/src/problem.rs | 40 ++- .../tests/axum_integration.rs | 2 +- libs/toolkit-canonical-errors/tests/error.rs | 14 +- .../toolkit-canonical-errors/tests/problem.rs | 27 +- .../tests/resource_error_macro.rs | 32 +- .../tests/showcase.rs | 84 ++--- ...ing_field_violation_on_invalid_argument.rs | 2 +- ...dition_violation_on_failed_precondition.rs | 2 +- ...g_quota_violation_on_resource_exhausted.rs | 2 +- .../tests/ui/missing_reason_on_aborted.rs | 2 +- .../ui/missing_reason_on_out_of_range.rs | 2 +- .../ui/missing_reason_on_permission_denied.rs | 2 +- .../tests/ui/missing_resource_on_not_found.rs | 2 +- libs/toolkit-gts-macros/Cargo.toml | 4 + libs/toolkit-gts-macros/src/lib.rs | 281 +++++++++++++-- libs/toolkit-gts/src/lib.rs | 12 +- libs/toolkit-gts/src/permission.rs | 2 +- libs/toolkit-gts/src/plugin.rs | 2 +- libs/toolkit-gts/tests/macro_integration.rs | 92 ++++- .../toolkit-gts/tests/prefix_customization.rs | 154 +++++++++ libs/toolkit-odata/Cargo.toml | 1 + libs/toolkit-odata/src/errors.rs | 2 +- libs/toolkit-odata/src/problem_mapping.rs | 3 +- libs/toolkit/src/api/operation_builder.rs | 7 +- libs/toolkit/src/client_hub.rs | 31 +- libs/toolkit/src/plugins/mod.rs | 49 +-- libs/toolkit/tests/integration_test.rs | 4 +- .../test_integration_seams.py | 5 +- testing/e2e/gears/oagw/test_authz.py | 8 +- .../test_types_registry_list.py | 11 +- tools/dylint_lints/Cargo.lock | 20 +- tools/dylint_lints/Cargo.toml | 6 +- .../src/lib.rs | 12 +- .../ui/gts_struct_schema_for.rs | 4 +- .../ui/gts_struct_schema_for.stderr | 2 +- .../de0901_gts_string_pattern/Cargo.toml | 1 + .../de0901_gts_string_pattern/README.md | 35 +- .../de0901_gts_string_pattern/src/lib.rs | 108 +++--- .../ui/invalid_cases.rs | 2 +- .../ui/invalid_cases.stderr | 2 +- .../ui/permission_strings.rs | 2 +- .../ui/permission_strings.stderr | 12 +- .../ui/valid_use_cases.rs | 44 ++- .../ui/gts_struct_schema_for.rs | 4 +- .../fixtures/all_features_true/Cargo.lock | 7 - .../tests/fixtures/excluded_crate/Cargo.lock | 7 - .../tests/fixtures/missing_docs_rs/Cargo.lock | 7 - .../tests/fixtures/publish_false/Cargo.lock | 7 - tools/fuzz/Cargo.lock | 325 +++++------------- tools/gts-analyze/Cargo.toml | 5 + tools/gts-analyze/src/scan/json.rs | 11 +- tools/gts-analyze/src/scan/mod.rs | 15 +- 278 files changed, 3271 insertions(+), 2466 deletions(-) create mode 100644 libs/toolkit-gts/tests/prefix_customization.rs delete mode 100644 tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/all_features_true/Cargo.lock delete mode 100644 tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/excluded_crate/Cargo.lock delete mode 100644 tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/missing_docs_rs/Cargo.lock delete mode 100644 tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/publish_false/Cargo.lock diff --git a/Cargo.lock b/Cargo.lock index 6271fdd03..293e92e3d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1302,6 +1302,7 @@ dependencies = [ "cf-gears-toolkit-canonical-errors", "cf-gears-toolkit-db", "cf-gears-toolkit-db-macros", + "cf-gears-toolkit-gts", "cf-gears-toolkit-macros", "cf-gears-toolkit-odata", "cf-gears-toolkit-odata-macros", @@ -1405,7 +1406,6 @@ dependencies = [ "cf-gears-toolkit-odata-macros", "cf-gears-toolkit-security", "gts", - "gts-macros", "schemars 1.2.1", "serde", "serde_json", @@ -1427,6 +1427,7 @@ dependencies = [ "cf-gears-authn-resolver-sdk", "cf-gears-toolkit", "cf-gears-toolkit-canonical-errors", + "cf-gears-toolkit-gts", "cf-gears-toolkit-http", "cf-gears-toolkit-macros", "cf-gears-toolkit-security", @@ -1572,6 +1573,7 @@ dependencies = [ "cf-gears-credstore-sdk", "cf-gears-toolkit", "cf-gears-toolkit-canonical-errors", + "cf-gears-toolkit-gts", "cf-gears-toolkit-macros", "cf-gears-toolkit-security", "cf-gears-types-registry-sdk", @@ -1688,8 +1690,10 @@ dependencies = [ "cf-gears-toolkit-canonical-errors", "cf-gears-toolkit-db", "cf-gears-toolkit-db-macros", + "cf-gears-toolkit-gts", "cf-gears-toolkit-macros", "cf-gears-toolkit-security", + "gts", "hex", "http", "infer", @@ -1714,6 +1718,7 @@ name = "cf-gears-file-storage-sdk" version = "0.1.0" dependencies = [ "cf-gears-toolkit-canonical-errors", + "cf-gears-toolkit-gts", "time", "uuid", ] @@ -1849,11 +1854,11 @@ name = "cf-gears-model-registry-sdk" version = "0.1.0" dependencies = [ "async-trait", + "cf-gears-toolkit-gts", "cf-gears-toolkit-odata", "cf-gears-toolkit-security", "chrono", "gts", - "gts-macros", "schemars 1.2.1", "serde", "serde_json", @@ -1871,6 +1876,7 @@ dependencies = [ "cf-gears-nodes-registry-sdk", "cf-gears-toolkit", "cf-gears-toolkit-canonical-errors", + "cf-gears-toolkit-gts", "cf-gears-toolkit-macros", "cf-gears-toolkit-node-info", "chrono", @@ -1888,6 +1894,7 @@ version = "0.1.22" dependencies = [ "async-trait", "cf-gears-toolkit-canonical-errors", + "cf-gears-toolkit-gts", "cf-gears-toolkit-node-info", "serde_json", "thiserror 2.0.18", @@ -1933,6 +1940,7 @@ dependencies = [ "cf-gears-toolkit", "cf-gears-toolkit-auth", "cf-gears-toolkit-canonical-errors", + "cf-gears-toolkit-gts", "cf-gears-toolkit-http", "cf-gears-toolkit-macros", "cf-gears-toolkit-security", @@ -1983,6 +1991,7 @@ dependencies = [ "axum", "bytes", "cf-gears-toolkit-canonical-errors", + "cf-gears-toolkit-gts", "cf-gears-toolkit-security", "futures-core", "futures-util", @@ -2008,6 +2017,7 @@ dependencies = [ "base64 0.22.1", "cf-gears-authn-resolver-sdk", "cf-gears-toolkit", + "cf-gears-toolkit-gts", "cf-gears-toolkit-macros", "cf-gears-toolkit-security", "cf-gears-types-registry-sdk", @@ -2049,6 +2059,7 @@ dependencies = [ "cf-gears-toolkit-canonical-errors", "cf-gears-toolkit-db", "cf-gears-toolkit-db-macros", + "cf-gears-toolkit-gts", "cf-gears-toolkit-macros", "cf-gears-toolkit-odata", "cf-gears-toolkit-security", @@ -2155,6 +2166,7 @@ dependencies = [ "cf-gears-toolkit-canonical-errors", "cf-gears-toolkit-db", "cf-gears-toolkit-db-macros", + "cf-gears-toolkit-gts", "cf-gears-toolkit-macros", "cf-gears-toolkit-security", "inventory", @@ -2226,6 +2238,7 @@ dependencies = [ "async-trait", "cf-gears-authz-resolver-sdk", "cf-gears-toolkit", + "cf-gears-toolkit-gts", "cf-gears-toolkit-macros", "cf-gears-toolkit-security", "cf-gears-types-registry-sdk", @@ -2265,6 +2278,7 @@ dependencies = [ "cf-gears-account-management-sdk", "cf-gears-toolkit", "cf-gears-toolkit-canonical-errors", + "cf-gears-toolkit-gts", "cf-gears-toolkit-macros", "cf-gears-toolkit-odata", "cf-gears-toolkit-security", @@ -2452,7 +2466,7 @@ version = "0.7.4" dependencies = [ "axum", "cf-gears-toolkit-canonical-errors-macro", - "gts-id", + "cf-gears-toolkit-gts", "http", "sea-orm", "serde", @@ -2467,6 +2481,7 @@ dependencies = [ name = "cf-gears-toolkit-canonical-errors-macro" version = "0.6.1" dependencies = [ + "gts-id", "proc-macro-crate", "proc-macro2", "quote", @@ -2545,6 +2560,8 @@ dependencies = [ name = "cf-gears-toolkit-gts-macros" version = "0.1.2" dependencies = [ + "gts", + "gts-id", "proc-macro-crate", "proc-macro2", "quote", @@ -2656,6 +2673,7 @@ dependencies = [ "base64 0.22.1", "bigdecimal", "cf-gears-toolkit-canonical-errors", + "cf-gears-toolkit-gts", "chrono", "chrono-tz", "peg", @@ -2744,6 +2762,7 @@ dependencies = [ "cf-gears-authz-resolver-sdk", "cf-gears-tenant-resolver-sdk", "cf-gears-toolkit", + "cf-gears-toolkit-gts", "cf-gears-toolkit-macros", "cf-gears-toolkit-security", "cf-gears-types-registry-sdk", @@ -2787,6 +2806,7 @@ version = "0.3.0" dependencies = [ "async-trait", "cf-gears-toolkit-canonical-errors", + "cf-gears-toolkit-gts", "gts", "serde_json", "thiserror 2.0.18", @@ -3654,7 +3674,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3835,7 +3855,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4329,9 +4349,9 @@ dependencies = [ [[package]] name = "gts" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78d5445277d7f0df598295f7e53608540ae5fe0b830d5010f8f7cce21c69d09f" +checksum = "04c85c9d9cd01c81990a8ccb5f9d584d83571f2d4df2ae6d974170c780bdf693" dependencies = [ "gts-id", "jsonschema", @@ -4342,7 +4362,6 @@ dependencies = [ "shellexpand", "thiserror 2.0.18", "tracing", - "uuid", "walkdir", ] @@ -4351,7 +4370,9 @@ name = "gts-analyze" version = "0.1.0" dependencies = [ "anyhow", + "cf-gears-toolkit-gts", "clap", + "gts-id", "proc-macro2", "regex", "serde", @@ -4362,18 +4383,19 @@ dependencies = [ [[package]] name = "gts-id" -version = "0.10.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7375046849cd80b1db1ed549272eb2fab826b13b13e9efb4333a6ec606070c8c" +checksum = "bb0e34e1934f1197b1cb68eb0020cb5aadd79a93b7b8f030d9ebee8dc5a027f2" dependencies = [ "thiserror 2.0.18", + "uuid", ] [[package]] name = "gts-macros" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20a84d8b5e87ab37b5539bb5e9d830b40235918f375405569c219f4de9b3f7d4" +checksum = "07961d01ac64b0ec00940d53a1361926cf759df8750fbaf236d43990e6718efe" dependencies = [ "gts-id", "proc-macro2", @@ -4825,7 +4847,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.58.0", + "windows-core 0.62.2", ] [[package]] @@ -5148,7 +5170,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6060,7 +6082,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -8166,7 +8188,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -8263,7 +8285,7 @@ dependencies = [ "security-framework 3.7.0", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -9602,7 +9624,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -10480,6 +10502,7 @@ version = "0.6.1" dependencies = [ "async-trait", "cf-gears-toolkit-canonical-errors", + "cf-gears-toolkit-gts", "cf-gears-toolkit-odata", "cf-gears-toolkit-odata-macros", "cf-gears-toolkit-sdk", @@ -10918,7 +10941,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 8778041aa..ac9eccf03 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -561,10 +561,11 @@ fnv = "1.0" jsonschema = { version = "0.40", default-features = false } walkdir = "2.5" -# GTS library -gts-id = "0.10.0" -gts = "0.10.0" -gts-macros = "0.10.0" +# GTS library. +# Keep in sync with tools/dylint_lints/Cargo.toml. +gts-id = "0.11.0" +gts = "0.11.0" +gts-macros = "0.11.0" [workspace.metadata.cargo-shear] ignored-paths = ["**/tests/ui/**"] diff --git a/docs/toolkit_unified_system/03_clienthub_and_plugins.md b/docs/toolkit_unified_system/03_clienthub_and_plugins.md index 35c871575..609d33b14 100644 --- a/docs/toolkit_unified_system/03_clienthub_and_plugins.md +++ b/docs/toolkit_unified_system/03_clienthub_and_plugins.md @@ -56,9 +56,10 @@ For plugin-like scenarios where multiple implementations of the same interface c ```rust use toolkit::client_hub::ClientScope; +use toolkit_gts::gts_id; // Plugin registers with a scope (e.g., GTS instance ID) -let scope = ClientScope::gts_id("gts.cf.toolkit.plugins.plugin.v1~vendor.pkg.my_gear.plugin.v1~acme.test._.plugin.v1"); +let scope = ClientScope::gts_id(gts_id!("cf.toolkit.plugins.plugin.v1~vendor.pkg.my_gear.plugin.v1~acme.test._.plugin.v1")); ctx.client_hub().register_scoped::(scope, plugin_impl); // Main gear resolves the selected plugin @@ -168,13 +169,13 @@ pub trait MyGearPluginClient: Send + Sync { ```rust // -sdk/src/gts.rs -use gts_macros::struct_to_gts_schema; -use toolkit::gts::PluginV1; +use toolkit_gts::gts_type_schema; +use toolkit_gts::PluginV1; -#[struct_to_gts_schema( +#[gts_type_schema( dir_path = "schemas", base = PluginV1, - schema_id = "gts.cf.toolkit.plugins.plugin.v1~cf.y.my_gear.plugin.v1~", + type_id = gts_id!("cf.toolkit.plugins.plugin.v1~cf.y.my_gear.plugin.v1~"), description = "My Gear plugin specification", properties = "" )] @@ -281,7 +282,7 @@ impl Service { async fn resolve_plugin(&self) -> Result { let registry = self.hub.get::()?; - let plugin_type_id = MyGearPluginSpecV1::gts_schema_id().clone(); + let plugin_type_id = MyGearPluginSpecV1::gts_type_id().clone(); let instances = registry .list(ListQuery::new() .with_pattern(format!("{plugin_type_id}*")) @@ -348,9 +349,9 @@ gears: ### Plugin Checklist - [ ] SDK defines both `Client` trait (public) and `PluginClient` trait (plugins) -- [ ] SDK defines GTS schema type with `#[struct_to_gts_schema]` +- [ ] SDK defines GTS schema type with `#[gts_type_schema]` (from `toolkit_gts`) - [ ] Main gear depends on `types-registry` but MUST NOT depend on plugin crates -- [ ] Main gear registers plugin **schema** using `gts_schema_with_refs_as_string()` +- [ ] Main gear registers plugin **schema** using `gts_type_schema_with_refs_as_string()` - [ ] Main gear registers public client WITHOUT scope - [ ] Main gear resolves plugin lazily (after types-registry is ready) - [ ] Each plugin depends on `types-registry` diff --git a/examples/toolkit/users-info/users-info-sdk/Cargo.toml b/examples/toolkit/users-info/users-info-sdk/Cargo.toml index d8a318193..1b559bf06 100644 --- a/examples/toolkit/users-info/users-info-sdk/Cargo.toml +++ b/examples/toolkit/users-info/users-info-sdk/Cargo.toml @@ -22,6 +22,7 @@ async-trait = { workspace = true } # Canonical error envelope (re-exported as `UsersInfoError`) toolkit-canonical-errors = { workspace = true } +toolkit-gts = { workspace = true } # OData macros (for #[derive(ODataFilterable)]) toolkit-odata-macros = { workspace = true, optional = true } diff --git a/examples/toolkit/users-info/users-info-sdk/src/gts.rs b/examples/toolkit/users-info/users-info-sdk/src/gts.rs index 77cf19ce8..205def298 100644 --- a/examples/toolkit/users-info/users-info-sdk/src/gts.rs +++ b/examples/toolkit/users-info/users-info-sdk/src/gts.rs @@ -19,15 +19,16 @@ //! impl-crate sites that call the macro therefore duplicate these //! literals. A test in the impl crate asserts the strings stay in sync. +use toolkit_gts::gts_id; /// `User` resource. Used as the `resource_type` field on canonical /// errors emitted from user-scoped operations (e.g. `user {id} not /// found` → 404). -pub const USER_RESOURCE_TYPE: &str = "gts.cf.example1.users.user.v1~"; +pub const USER_RESOURCE_TYPE: &str = gts_id!("cf.example1.users.user.v1~"); /// `City` resource. Used as the `resource_type` field on canonical /// errors emitted from city-scoped operations. -pub const CITY_RESOURCE_TYPE: &str = "gts.cf.example1.users.city.v1~"; +pub const CITY_RESOURCE_TYPE: &str = gts_id!("cf.example1.users.city.v1~"); /// `Address` resource. Used as the `resource_type` field on canonical /// errors emitted from address-scoped operations. -pub const ADDRESS_RESOURCE_TYPE: &str = "gts.cf.example1.users.address.v1~"; +pub const ADDRESS_RESOURCE_TYPE: &str = gts_id!("cf.example1.users.address.v1~"); diff --git a/examples/toolkit/users-info/users-info/src/api/rest/error.rs b/examples/toolkit/users-info/users-info/src/api/rest/error.rs index 0d75bee2f..cc51e5aa2 100644 --- a/examples/toolkit/users-info/users-info/src/api/rest/error.rs +++ b/examples/toolkit/users-info/users-info/src/api/rest/error.rs @@ -2,13 +2,13 @@ use toolkit::api::canonical_prelude::*; use crate::domain::error::DomainError; -#[resource_error("gts.cf.example1.users.user.v1~")] +#[resource_error(gts_id!("cf.example1.users.user.v1~"))] struct UserResourceError; -#[resource_error("gts.cf.example1.users.city.v1~")] +#[resource_error(gts_id!("cf.example1.users.city.v1~"))] struct CityResourceError; -#[resource_error("gts.cf.example1.users.address.v1~")] +#[resource_error(gts_id!("cf.example1.users.address.v1~"))] struct AddressResourceError; fn map_entity_not_found(entity_type: &str, id: String) -> CanonicalError { diff --git a/examples/toolkit/users-info/users-info/src/api/rest/routes/mod.rs b/examples/toolkit/users-info/users-info/src/api/rest/routes/mod.rs index 932b9bc50..4c85ee575 100644 --- a/examples/toolkit/users-info/users-info/src/api/rest/routes/mod.rs +++ b/examples/toolkit/users-info/users-info/src/api/rest/routes/mod.rs @@ -28,7 +28,7 @@ use crate::gear::ConcreteAppServices; use axum::Router; use std::sync::Arc; use toolkit::api::OpenApiRegistry; -use toolkit::api::operation_builder::LicenseFeature; +use toolkit::api::operation_builder::{CORE_GLOBAL_BASE_LICENSE_FEATURE, LicenseFeature}; mod addresses; mod cities; @@ -39,7 +39,7 @@ pub(super) struct License; impl AsRef for License { fn as_ref(&self) -> &'static str { - "gts.cf.core.lic.feat.v1~cf.core.global.base.v1" + CORE_GLOBAL_BASE_LICENSE_FEATURE } } diff --git a/gears/chat-engine/chat-engine/Cargo.toml b/gears/chat-engine/chat-engine/Cargo.toml index 144cfb347..56a6120e4 100644 --- a/gears/chat-engine/chat-engine/Cargo.toml +++ b/gears/chat-engine/chat-engine/Cargo.toml @@ -63,6 +63,7 @@ toolkit-db-macros = { workspace = true } toolkit-security = { workspace = true } toolkit-macros = { workspace = true } toolkit-canonical-errors = { workspace = true } +toolkit-gts = { workspace = true } toolkit-odata = { workspace = true, features = ["with-utoipa"] } toolkit-odata-macros = { workspace = true } diff --git a/gears/chat-engine/chat-engine/src/api/rest/error.rs b/gears/chat-engine/chat-engine/src/api/rest/error.rs index 0d7b5f6c7..9d9645902 100644 --- a/gears/chat-engine/chat-engine/src/api/rest/error.rs +++ b/gears/chat-engine/chat-engine/src/api/rest/error.rs @@ -35,20 +35,20 @@ use crate::domain::error::ChatEngineError; // --------------------------------------------------------------------------- /// Session-scoped error type used for not-found / conflict mappings. -#[resource_error("gts.cf.core.chat_engine.session.v1~")] +#[resource_error(gts_id!("cf.core.chat_engine.session.v1~"))] pub struct ChatEngineSessionError; /// Message-scoped error type. -#[resource_error("gts.cf.core.chat_engine.message.v1~")] +#[resource_error(gts_id!("cf.core.chat_engine.message.v1~"))] pub struct ChatEngineMessageError; /// Plugin-scoped error type (used by `BackendUnavailable`). -#[resource_error("gts.cf.core.chat_engine.plugin.v1~")] +#[resource_error(gts_id!("cf.core.chat_engine.plugin.v1~"))] pub struct ChatEnginePluginError; /// Generic chat-engine resource type used when the inner variant lacks a /// more precise scope. -#[resource_error("gts.cf.core.chat_engine.resource.v1~")] +#[resource_error(gts_id!("cf.core.chat_engine.resource.v1~"))] pub struct ChatEngineResourceError; // --------------------------------------------------------------------------- diff --git a/gears/chat-engine/chat-engine/src/api/rest/routes/mod.rs b/gears/chat-engine/chat-engine/src/api/rest/routes/mod.rs index afee8698b..d844244cf 100644 --- a/gears/chat-engine/chat-engine/src/api/rest/routes/mod.rs +++ b/gears/chat-engine/chat-engine/src/api/rest/routes/mod.rs @@ -21,7 +21,9 @@ use std::sync::Arc; use axum::{Extension, Router}; use http::StatusCode; use toolkit::api::OpenApiRegistry; -use toolkit::api::operation_builder::{LicenseFeature, OperationBuilder}; +use toolkit::api::operation_builder::{ + CORE_GLOBAL_BASE_LICENSE_FEATURE, LicenseFeature, OperationBuilder, +}; use crate::api::rest::WebhookEmitter; use crate::api::rest::dto::{ @@ -44,13 +46,12 @@ const API_TAG: &str = "Chat Engine"; /// License feature required by all `cf-chat-engine` endpoints. /// -/// Mirrors the gating policy used by sibling modules -/// (`gts.cf.core.lic.feat.v1~cf.core.global.base.v1`). +/// Mirrors the gating policy used by sibling modules. pub(crate) struct ChatEngineLicense; impl AsRef for ChatEngineLicense { fn as_ref(&self) -> &'static str { - "gts.cf.core.lic.feat.v1~cf.core.global.base.v1" + CORE_GLOBAL_BASE_LICENSE_FEATURE } } diff --git a/gears/chat-engine/chat-engine/src/infra/webhook_compat_tests.rs b/gears/chat-engine/chat-engine/src/infra/webhook_compat_tests.rs index 702dbbebb..b4aebf1e0 100644 --- a/gears/chat-engine/chat-engine/src/infra/webhook_compat_tests.rs +++ b/gears/chat-engine/chat-engine/src/infra/webhook_compat_tests.rs @@ -1,6 +1,7 @@ use super::*; use std::time::Duration; use tokio_util::sync::CancellationToken; +use toolkit_gts::gts_id; use uuid::Uuid; use chat_engine_sdk::models::{TenantId, UserId}; @@ -191,8 +192,9 @@ fn status_mapping_table() { #[test] fn plugin_instance_id_returns_constructor_value() { - let p = WebhookCompatPlugin::with_client("gts.cf.webhook.v1~vendor", Client::new()); - assert_eq!(p.plugin_instance_id(), "gts.cf.webhook.v1~vendor"); + const PLUGIN_ID: &str = gts_id!("cf.webhook.compat.plugin.v1~vendor.webhook.test.instance.v1~"); + let p = WebhookCompatPlugin::with_client(PLUGIN_ID, Client::new()); + assert_eq!(p.plugin_instance_id(), PLUGIN_ID); } #[tokio::test] diff --git a/gears/credstore/credstore-sdk/src/gts.rs b/gears/credstore/credstore-sdk/src/gts.rs index 37b03b127..f90be4764 100644 --- a/gears/credstore/credstore-sdk/src/gts.rs +++ b/gears/credstore/credstore-sdk/src/gts.rs @@ -5,7 +5,7 @@ use toolkit_gts::gts_type_schema; #[gts_type_schema( dir_path = "schemas", base = PluginV1, - type_id = "gts.cf.toolkit.plugins.plugin.v1~cf.core.credstore.plugin.v1~", + type_id = gts_id!("cf.toolkit.plugins.plugin.v1~cf.core.credstore.plugin.v1~"), description = "CredStore plugin specification", properties = "", )] diff --git a/gears/credstore/credstore/Cargo.toml b/gears/credstore/credstore/Cargo.toml index 1a04b9945..daab6ea78 100644 --- a/gears/credstore/credstore/Cargo.toml +++ b/gears/credstore/credstore/Cargo.toml @@ -39,4 +39,5 @@ toolkit-security = { workspace = true } toolkit-macros = { workspace = true } [dev-dependencies] +toolkit-gts = { workspace = true } types-registry-sdk = { workspace = true, features = ["test-util"] } diff --git a/gears/credstore/credstore/src/domain/error_tests.rs b/gears/credstore/credstore/src/domain/error_tests.rs index 05df8d295..5543bf7aa 100644 --- a/gears/credstore/credstore/src/domain/error_tests.rs +++ b/gears/credstore/credstore/src/domain/error_tests.rs @@ -1,5 +1,6 @@ // Created: 2026-04-07 by Constructor Tech use toolkit::plugins::ChoosePluginError; +use toolkit_gts::gts_id; use super::*; @@ -42,7 +43,7 @@ fn from_serde_json_error_becomes_internal() { #[test] fn from_choose_plugin_error_not_found_becomes_plugin_not_found() { let src = ChoosePluginError::PluginNotFound { - type_id: "gts.cf.core.test.plugin.v1~".into(), + type_id: gts_id!("cf.core.test.plugin.v1~").into(), vendor: "acme".into(), }; let dst = DomainError::from(src); @@ -52,13 +53,13 @@ fn from_choose_plugin_error_not_found_becomes_plugin_not_found() { #[test] fn from_choose_plugin_error_invalid_instance_becomes_invalid_plugin_instance() { let src = ChoosePluginError::InvalidPluginInstance { - gts_id: "gts.cf.core.test.error.v1~".into(), + gts_id: gts_id!("cf.core.test.error.v1~").into(), reason: "bad content".into(), }; let dst = DomainError::from(src); assert!( matches!(dst, DomainError::InvalidPluginInstance { gts_id, reason } - if gts_id == "gts.cf.core.test.error.v1~" && reason == "bad content") + if gts_id == gts_id!("cf.core.test.error.v1~") && reason == "bad content") ); } @@ -112,28 +113,30 @@ fn domain_plugin_not_found_becomes_no_plugin_available() { #[test] fn domain_invalid_plugin_instance_becomes_internal() { + const TEST_ERROR_ID: &str = gts_id!("cf.core.test.error.v1~"); let src = DomainError::InvalidPluginInstance { - gts_id: "gts.cf.core.test.error.v1~".into(), + gts_id: TEST_ERROR_ID.into(), reason: "bad".into(), }; let dst = CredStoreError::from(src); assert!( matches!(dst, CredStoreError::Internal(ref msg) - if msg.contains("gts.cf.core.test.error.v1~") && msg.contains("bad")), + if msg.contains(TEST_ERROR_ID) && msg.contains("bad")), "expected Internal with gts_id and reason, got: {dst:?}" ); } #[test] fn domain_plugin_unavailable_becomes_service_unavailable() { + const TEST_ERROR_ID: &str = gts_id!("cf.core.test.error.v1~"); let src = DomainError::PluginUnavailable { - gts_id: "gts.cf.core.test.error.v1~".into(), + gts_id: TEST_ERROR_ID.into(), reason: "not ready".into(), }; let dst = CredStoreError::from(src); assert!( matches!(dst, CredStoreError::ServiceUnavailable(ref msg) - if msg.contains("gts.cf.core.test.error.v1~") && msg.contains("not ready")), + if msg.contains(TEST_ERROR_ID) && msg.contains("not ready")), "expected ServiceUnavailable with gts_id and reason, got: {dst:?}" ); } diff --git a/gears/file-parser/src/api/rest/error.rs b/gears/file-parser/src/api/rest/error.rs index 229f8808e..86f734ff9 100644 --- a/gears/file-parser/src/api/rest/error.rs +++ b/gears/file-parser/src/api/rest/error.rs @@ -2,7 +2,7 @@ use toolkit_canonical_errors::{CanonicalError, resource_error}; use crate::domain::error::DomainError; -#[resource_error("gts.cf.file_parser.parser.file.v1~")] +#[resource_error(gts_id!("cf.file_parser.parser.file.v1~"))] pub struct FileParserError; impl From for CanonicalError { diff --git a/gears/file-parser/src/api/rest/routes.rs b/gears/file-parser/src/api/rest/routes.rs index d7a77cc35..424fe169e 100644 --- a/gears/file-parser/src/api/rest/routes.rs +++ b/gears/file-parser/src/api/rest/routes.rs @@ -2,13 +2,16 @@ use crate::api::rest::handlers; use crate::domain::service::FileParserService; use axum::{Extension, Router}; use std::sync::Arc; -use toolkit::api::{OpenApiRegistry, OperationBuilder, operation_builder::LicenseFeature}; +use toolkit::api::{ + OpenApiRegistry, OperationBuilder, + operation_builder::{CORE_GLOBAL_BASE_LICENSE_FEATURE, LicenseFeature}, +}; struct License; impl AsRef for License { fn as_ref(&self) -> &'static str { - "gts.cf.core.lic.feat.v1~cf.core.global.base.v1" + CORE_GLOBAL_BASE_LICENSE_FEATURE } } diff --git a/gears/file-storage/file-storage-sdk/Cargo.toml b/gears/file-storage/file-storage-sdk/Cargo.toml index 4344208da..e9639608c 100644 --- a/gears/file-storage/file-storage-sdk/Cargo.toml +++ b/gears/file-storage/file-storage-sdk/Cargo.toml @@ -23,3 +23,4 @@ time = { workspace = true } # Canonical error envelope (re-exported as `FileStorageError`) toolkit-canonical-errors = { workspace = true } +toolkit-gts = { workspace = true } diff --git a/gears/file-storage/file-storage-sdk/src/gts.rs b/gears/file-storage/file-storage-sdk/src/gts.rs index 3a29af2e7..803881a76 100644 --- a/gears/file-storage/file-storage-sdk/src/gts.rs +++ b/gears/file-storage/file-storage-sdk/src/gts.rs @@ -1,8 +1,10 @@ //! GTS resource-type constants for the file-storage gear. +use toolkit_gts::gts_id; + /// GTS file-type resource family used by the Authorization Service to make /// per-type access decisions (PRD `cpt-cf-file-storage-fr-file-type-classification`). -pub const FILE_TYPE_RESOURCE: &str = "gts.cf.fstorage.file.type.v1~"; +pub const FILE_TYPE_RESOURCE: &str = gts_id!("cf.fstorage.file.type.v1~"); #[cfg(test)] #[path = "gts_tests.rs"] diff --git a/gears/file-storage/file-storage-sdk/src/gts_tests.rs b/gears/file-storage/file-storage-sdk/src/gts_tests.rs index a27dc55d5..b58627cce 100644 --- a/gears/file-storage/file-storage-sdk/src/gts_tests.rs +++ b/gears/file-storage/file-storage-sdk/src/gts_tests.rs @@ -1,16 +1,17 @@ use super::*; +use toolkit_gts::gts_id; #[test] fn file_type_resource_has_exact_expected_value() { // Pinning the literal: the Authorization Service matches on this string, so // a silent change here would break per-type access decisions. - assert_eq!(FILE_TYPE_RESOURCE, "gts.cf.fstorage.file.type.v1~"); + assert_eq!(FILE_TYPE_RESOURCE, gts_id!("cf.fstorage.file.type.v1~")); } #[test] fn file_type_resource_follows_gts_type_format() { assert!( - FILE_TYPE_RESOURCE.starts_with("gts.cf.fstorage.file.type.v1"), + FILE_TYPE_RESOURCE.starts_with(gts_id!("cf.fstorage.file.type.v1~")), "must be the fstorage file-type family: {FILE_TYPE_RESOURCE}" ); assert!( diff --git a/gears/file-storage/file-storage/Cargo.toml b/gears/file-storage/file-storage/Cargo.toml index 7141b79b7..94883c03f 100644 --- a/gears/file-storage/file-storage/Cargo.toml +++ b/gears/file-storage/file-storage/Cargo.toml @@ -68,6 +68,8 @@ toolkit-db-macros = { workspace = true } toolkit-security = { workspace = true } toolkit-canonical-errors = { workspace = true, features = ["axum", "utoipa"] } toolkit-macros = { workspace = true } +toolkit-gts = { workspace = true } +gts = { workspace = true } [dev-dependencies] serde_json = { workspace = true } diff --git a/gears/file-storage/file-storage/src/api/rest/error.rs b/gears/file-storage/file-storage/src/api/rest/error.rs index 9f7e8c97c..23ea46e69 100644 --- a/gears/file-storage/file-storage/src/api/rest/error.rs +++ b/gears/file-storage/file-storage/src/api/rest/error.rs @@ -4,7 +4,7 @@ use toolkit::api::canonical_prelude::*; use crate::domain::error::DomainError; -#[resource_error("gts.cf.fstorage.file.object.v1~")] +#[resource_error(gts_id!("cf.fstorage.file.object.v1~"))] struct FileResourceError; impl From for CanonicalError { diff --git a/gears/file-storage/file-storage/src/api/rest/routes.rs b/gears/file-storage/file-storage/src/api/rest/routes.rs index 64c2d6afa..47b401291 100644 --- a/gears/file-storage/file-storage/src/api/rest/routes.rs +++ b/gears/file-storage/file-storage/src/api/rest/routes.rs @@ -8,6 +8,7 @@ use axum::Router; use http::StatusCode; use toolkit::api::OpenApiRegistry; use toolkit::api::operation_builder::{LicenseFeature, OperationBuilder}; +use toolkit_gts::gts_id; use super::dto; use super::handlers; @@ -23,7 +24,7 @@ pub(crate) struct License; impl AsRef for License { fn as_ref(&self) -> &'static str { - "gts.cf.core.lic.feat.v1~cf.core.global.base.v1" + gts_id!("cf.core.lic.feat.v1~cf.core.global.base.v1") } } diff --git a/gears/file-storage/file-storage/src/domain/multipart_service.rs b/gears/file-storage/file-storage/src/domain/multipart_service.rs index a0ac191af..f34306119 100644 --- a/gears/file-storage/file-storage/src/domain/multipart_service.rs +++ b/gears/file-storage/file-storage/src/domain/multipart_service.rs @@ -19,6 +19,7 @@ use std::sync::Arc; use time::OffsetDateTime; +use toolkit_gts::gts_id; use toolkit_security::{AccessScope, SecurityContext}; use uuid::Uuid; @@ -36,7 +37,7 @@ use crate::infra::signed_url::{Claims, Issuer, MultipartClaims, Op, UploadConstr /// Quota metric name (duplicated from service.rs; both refer to the same /// platform metric — no abstraction needed here). -const QUOTA_METRIC_NAME: &str = "gts.cf.qe.metric.type.v1~cf.qe.metric.file_storage_bytes.v1"; +const QUOTA_METRIC_NAME: &str = gts_id!("cf.qe.metric.type.v1~cf.qe.metric.file_storage_bytes.v1"); /// The multipart-upload service (multipart-coordinator feature). /// diff --git a/gears/file-storage/file-storage/src/domain/service/mod.rs b/gears/file-storage/file-storage/src/domain/service/mod.rs index b57a50b1d..be48f52de 100644 --- a/gears/file-storage/file-storage/src/domain/service/mod.rs +++ b/gears/file-storage/file-storage/src/domain/service/mod.rs @@ -26,6 +26,7 @@ use std::sync::Arc; use time::OffsetDateTime; +use toolkit_gts::gts_id; use toolkit_security::{AccessScope, SecurityContext}; use uuid::Uuid; @@ -81,7 +82,7 @@ pub struct DownloadTicket { /// Quota metric name used for storage preflight checks. /// @cpt-cf-file-storage-fr-storage-quota pub(super) const QUOTA_METRIC_NAME: &str = - "gts.cf.qe.metric.type.v1~cf.qe.metric.file_storage_bytes.v1"; + gts_id!("cf.qe.metric.type.v1~cf.qe.metric.file_storage_bytes.v1"); /// The control-plane file service. #[allow(unknown_lints, de0309_must_have_domain_model)] @@ -135,7 +136,7 @@ impl FileService { } pub(super) fn validate_gts_type(t: &str) -> Result<(), DomainError> { - if t.starts_with("gts.") && t.contains('~') { + if gts::GtsTypeId::try_new(t).is_ok() { Ok(()) } else { Err(DomainError::invalid_gts_type(t)) diff --git a/gears/file-storage/file-storage/src/infra/external_clients.rs b/gears/file-storage/file-storage/src/infra/external_clients.rs index 3e515ec81..7b1f166b4 100644 --- a/gears/file-storage/file-storage/src/infra/external_clients.rs +++ b/gears/file-storage/file-storage/src/infra/external_clients.rs @@ -34,7 +34,7 @@ pub trait QuotaClient: Send + Sync { /// `additional_bytes` more. Returns `Allowed` or `Denied`. /// /// `metric_name` is the metric identifier used in the quota system - /// (e.g. `"gts.cf.qe.metric.type.v1~cf.qe.metric.file_storage_bytes.v1"`). + /// (e.g. `gts_id!("cf.qe.metric.type.v1~cf.qe.metric.file_storage_bytes.v1")`). async fn check_storage_quota( &self, tenant_id: Uuid, diff --git a/gears/file-storage/file-storage/tests/audit_test.rs b/gears/file-storage/file-storage/tests/audit_test.rs index 69b041e17..2317112e8 100644 --- a/gears/file-storage/file-storage/tests/audit_test.rs +++ b/gears/file-storage/file-storage/tests/audit_test.rs @@ -18,6 +18,7 @@ use bytes::Bytes; use sea_orm_migration::MigratorTrait; use toolkit_db::migration_runner::run_migrations_for_testing; use toolkit_db::{ConnectOpts, DBProvider, DbError, connect_db}; +use toolkit_gts::gts_id; use toolkit_security::SecurityContext; use uuid::Uuid; @@ -33,7 +34,7 @@ use file_storage::infra::storage::Store; use file_storage::infra::storage::migrations::Migrator; use file_storage_sdk::{CustomMetadataPatch, NewFile, OwnerKind}; -const GTS: &str = "gts.cf.fstorage.file.type.v1~x.test.v1~"; +const GTS: &str = gts_id!("cf.fstorage.file.type.v1~x.test.file.type.v1~"); async fn build_db() -> Arc> { let mut path = std::env::temp_dir(); diff --git a/gears/file-storage/file-storage/tests/cleanup_test.rs b/gears/file-storage/file-storage/tests/cleanup_test.rs index 091c6f33a..b746b5b5a 100644 --- a/gears/file-storage/file-storage/tests/cleanup_test.rs +++ b/gears/file-storage/file-storage/tests/cleanup_test.rs @@ -22,6 +22,7 @@ use bytes::Bytes; use sea_orm_migration::MigratorTrait; use toolkit_db::migration_runner::run_migrations_for_testing; use toolkit_db::{ConnectOpts, DBProvider, DbError, connect_db}; +use toolkit_gts::gts_id; use toolkit_security::SecurityContext; use uuid::Uuid; @@ -39,7 +40,7 @@ use file_storage::infra::storage::Store; use file_storage::infra::storage::migrations::Migrator; use file_storage_sdk::{NewFile, OwnerKind}; -const GTS: &str = "gts.cf.fstorage.file.type.v1~x.cleanup-test.v1~"; +const GTS: &str = gts_id!("cf.fstorage.file.type.v1~x.cleanup_test.file.type.v1~"); // ── test harness ────────────────────────────────────────────────────────────── diff --git a/gears/file-storage/file-storage/tests/enforce_test.rs b/gears/file-storage/file-storage/tests/enforce_test.rs index bc87b35c4..70d2703da 100644 --- a/gears/file-storage/file-storage/tests/enforce_test.rs +++ b/gears/file-storage/file-storage/tests/enforce_test.rs @@ -18,6 +18,7 @@ use async_trait::async_trait; use sea_orm_migration::MigratorTrait; use toolkit_db::migration_runner::run_migrations_for_testing; use toolkit_db::{ConnectOpts, DBProvider, DbError, connect_db}; +use toolkit_gts::gts_id; use toolkit_security::SecurityContext; use uuid::Uuid; @@ -35,7 +36,7 @@ use file_storage::infra::storage::Store; use file_storage::infra::storage::migrations::Migrator; use file_storage_sdk::{CustomMetadataEntry, CustomMetadataPatch, NewFile, OwnerKind}; -const GTS: &str = "gts.cf.fstorage.file.type.v1~x.test.v1~"; +const GTS: &str = gts_id!("cf.fstorage.file.type.v1~x.test.file.type.v1~"); /// A mock quota client that denies once the cumulative requested bytes exceed a /// cap. Each `check_storage_quota` call counts as a request of `additional_bytes`. diff --git a/gears/file-storage/file-storage/tests/migration_test.rs b/gears/file-storage/file-storage/tests/migration_test.rs index dea42249e..fdd8c4144 100644 --- a/gears/file-storage/file-storage/tests/migration_test.rs +++ b/gears/file-storage/file-storage/tests/migration_test.rs @@ -9,13 +9,14 @@ use sea_orm::{ConnectionTrait, Database, DatabaseConnection, Statement}; use sea_orm_migration::MigratorTrait; +use toolkit_gts::gts_id; use file_storage::Migrator; const TENANT: &str = "00000000-0000-0000-0000-0000000000a1"; const OWNER: &str = "00000000-0000-0000-0000-0000000000b1"; const FILE: &str = "00000000-0000-0000-0000-0000000000c1"; -const GTS: &str = "gts.cf.fstorage.file.type.v1~x.test.v1~"; +const GTS: &str = gts_id!("cf.fstorage.file.type.v1~x.test.file.type.v1~"); /// 32 zero bytes — the only hash length the P1 `SHA-256` CHECK accepts. const HASH32: &str = "0000000000000000000000000000000000000000000000000000000000000000"; diff --git a/gears/file-storage/file-storage/tests/multipart_test.rs b/gears/file-storage/file-storage/tests/multipart_test.rs index 50063d980..050ea7603 100644 --- a/gears/file-storage/file-storage/tests/multipart_test.rs +++ b/gears/file-storage/file-storage/tests/multipart_test.rs @@ -21,6 +21,7 @@ use bytes::Bytes; use sea_orm_migration::MigratorTrait; use toolkit_db::migration_runner::run_migrations_for_testing; use toolkit_db::{ConnectOpts, DBProvider, DbError, connect_db}; +use toolkit_gts::gts_id; use toolkit_security::SecurityContext; use uuid::Uuid; @@ -41,7 +42,7 @@ use file_storage::infra::storage::Store; use file_storage::infra::storage::migrations::Migrator; use file_storage_sdk::{NewFile, OwnerKind}; -const GTS: &str = "gts.cf.fstorage.file.type.v1~x.test.v1~"; +const GTS: &str = gts_id!("cf.fstorage.file.type.v1~x.test.file.type.v1~"); async fn build_db() -> Arc> { let mut path = std::env::temp_dir(); diff --git a/gears/file-storage/file-storage/tests/ownership_test.rs b/gears/file-storage/file-storage/tests/ownership_test.rs index 5fc7811a3..53417038f 100644 --- a/gears/file-storage/file-storage/tests/ownership_test.rs +++ b/gears/file-storage/file-storage/tests/ownership_test.rs @@ -24,6 +24,7 @@ use bytes::Bytes; use sea_orm_migration::MigratorTrait; use toolkit_db::migration_runner::run_migrations_for_testing; use toolkit_db::{ConnectOpts, DBProvider, DbError, connect_db}; +use toolkit_gts::gts_id; use toolkit_security::SecurityContext; use uuid::Uuid; @@ -38,7 +39,7 @@ use file_storage::infra::storage::Store; use file_storage::infra::storage::migrations::Migrator; use file_storage_sdk::{NewFile, OwnerKind}; -const GTS: &str = "gts.cf.fstorage.file.type.v1~x.test.v1~"; +const GTS: &str = gts_id!("cf.fstorage.file.type.v1~x.test.file.type.v1~"); async fn build_db() -> Arc> { let mut path = std::env::temp_dir(); diff --git a/gears/file-storage/file-storage/tests/service_test.rs b/gears/file-storage/file-storage/tests/service_test.rs index efab8bcb5..f1568defc 100644 --- a/gears/file-storage/file-storage/tests/service_test.rs +++ b/gears/file-storage/file-storage/tests/service_test.rs @@ -11,6 +11,7 @@ use bytes::Bytes; use sea_orm_migration::MigratorTrait; use toolkit_db::migration_runner::run_migrations_for_testing; use toolkit_db::{ConnectOpts, DBProvider, DbError, connect_db}; +use toolkit_gts::gts_id; use toolkit_security::SecurityContext; use uuid::Uuid; @@ -26,7 +27,7 @@ use file_storage::infra::storage::Store; use file_storage::infra::storage::migrations::Migrator; use file_storage_sdk::{CustomMetadataEntry, CustomMetadataPatch, NewFile, OwnerFilter, OwnerKind}; -const GTS: &str = "gts.cf.fstorage.file.type.v1~x.test.v1~"; +const GTS: &str = gts_id!("cf.fstorage.file.type.v1~x.test.file.type.v1~"); async fn build_service() -> (Arc, DataPlaneService) { // A unique temp *file* DB: the service opens a connection per call, so every diff --git a/gears/mini-chat/mini-chat-sdk/src/gts.rs b/gears/mini-chat/mini-chat-sdk/src/gts.rs index b114fcd67..202f7c182 100644 --- a/gears/mini-chat/mini-chat-sdk/src/gts.rs +++ b/gears/mini-chat/mini-chat-sdk/src/gts.rs @@ -1,6 +1,18 @@ use toolkit::gts::PluginV1; +use toolkit_gts::gts_id; use toolkit_gts::gts_type_schema; +/// Resource type for the mini-chat chat surface. +pub const CHAT_RESOURCE_TYPE: &str = gts_id!("cf.core.ai_chat.chat.v1~cf.core.mini_chat.chat.v1~"); + +/// Resource type for the mini-chat model surface. +pub const MODEL_RESOURCE_TYPE: &str = + gts_id!("cf.core.ai_chat.model.v1~cf.core.mini_chat.model.v1~"); + +/// Resource type for the mini-chat user-quota surface. +pub const USER_QUOTA_RESOURCE_TYPE: &str = + gts_id!("cf.core.ai_chat.user_quota.v1~cf.core.mini_chat.user_quota.v1~"); + /// GTS type definition for mini-chat policy plugin instances. /// /// Each plugin registers an instance of this type with its vendor-specific @@ -16,7 +28,7 @@ use toolkit_gts::gts_type_schema; #[gts_type_schema( dir_path = "schemas", base = PluginV1, - type_id = "gts.cf.toolkit.plugins.plugin.v1~cf.core.mini_chat_model_policy.plugin.v1~", + type_id = gts_id!("cf.toolkit.plugins.plugin.v1~cf.core.mini_chat_model_policy.plugin.v1~"), description = "Mini-Chat Policy plugin specification", properties = "", )] @@ -33,7 +45,7 @@ pub struct MiniChatModelPolicyPluginSpecV1; #[gts_type_schema( dir_path = "schemas", base = PluginV1, - type_id = "gts.cf.toolkit.plugins.plugin.v1~cf.core.mini_chat_audit.plugin.v1~", + type_id = gts_id!("cf.toolkit.plugins.plugin.v1~cf.core.mini_chat_audit.plugin.v1~"), description = "Mini-Chat Audit plugin specification", properties = "", )] diff --git a/gears/mini-chat/mini-chat-sdk/src/lib.rs b/gears/mini-chat/mini-chat-sdk/src/lib.rs index 5f7f31294..6a8474389 100644 --- a/gears/mini-chat/mini-chat-sdk/src/lib.rs +++ b/gears/mini-chat/mini-chat-sdk/src/lib.rs @@ -10,6 +10,7 @@ pub use audit_models::{ TurnMutationAuditEvent, TurnMutationAuditEventType, TurnRetryAuditEvent, }; pub use error::{MiniChatAuditPluginError, MiniChatModelPolicyPluginError, PublishError}; +pub use gts::{CHAT_RESOURCE_TYPE, MODEL_RESOURCE_TYPE, USER_QUOTA_RESOURCE_TYPE}; pub use gts::{MiniChatAuditPluginSpecV1, MiniChatModelPolicyPluginSpecV1}; pub use models::{ EstimationBudgets, KillSwitches, ModelApiParams, ModelCatalogEntry, ModelGeneralConfig, diff --git a/gears/mini-chat/mini-chat/src/api/rest/error.rs b/gears/mini-chat/mini-chat/src/api/rest/error.rs index d1d0db1cf..300c0c08c 100644 --- a/gears/mini-chat/mini-chat/src/api/rest/error.rs +++ b/gears/mini-chat/mini-chat/src/api/rest/error.rs @@ -18,23 +18,23 @@ use crate::domain::service::{MutationError, StreamError}; // --------------------------------------------------------------------------- /// Errors attributable to a chat as a resource. -#[resource_error("gts.cf.core.mini_chat.chat.v1~")] +#[resource_error(gts_id!("cf.core.mini_chat.chat.v1~"))] pub struct MiniChatChatError; /// Errors attributable to a message as a resource. -#[resource_error("gts.cf.core.mini_chat.message.v1~")] +#[resource_error(gts_id!("cf.core.mini_chat.message.v1~"))] pub struct MiniChatMessageError; /// Errors attributable to a turn as a resource. -#[resource_error("gts.cf.core.mini_chat.turn.v1~")] +#[resource_error(gts_id!("cf.core.mini_chat.turn.v1~"))] pub struct MiniChatTurnError; /// Errors attributable to an attachment as a resource. -#[resource_error("gts.cf.core.mini_chat.attachment.v1~")] +#[resource_error(gts_id!("cf.core.mini_chat.attachment.v1~"))] pub struct MiniChatAttachmentError; /// Errors attributable to a model as a resource. -#[resource_error("gts.cf.core.mini_chat.model.v1~")] +#[resource_error(gts_id!("cf.core.mini_chat.model.v1~"))] pub struct MiniChatModelError; // --------------------------------------------------------------------------- @@ -351,6 +351,7 @@ impl From for CanonicalError { mod tests { use super::*; use toolkit_canonical_errors::Problem; + use toolkit_gts::{gts_id, gts_uri}; use uuid::Uuid; /// Build the wire `Problem` the canonical error middleware would emit @@ -371,25 +372,25 @@ mod tests { } } - const NOT_FOUND_TYPE: &str = "gts://gts.cf.core.errors.err.v1~cf.core.err.not_found.v1~"; + const NOT_FOUND_TYPE: &str = gts_uri!("cf.core.errors.err.v1~cf.core.err.not_found.v1~"); const INVALID_ARGUMENT_TYPE: &str = - "gts://gts.cf.core.errors.err.v1~cf.core.err.invalid_argument.v1~"; - const OUT_OF_RANGE_TYPE: &str = "gts://gts.cf.core.errors.err.v1~cf.core.err.out_of_range.v1~"; + gts_uri!("cf.core.errors.err.v1~cf.core.err.invalid_argument.v1~"); + const OUT_OF_RANGE_TYPE: &str = gts_uri!("cf.core.errors.err.v1~cf.core.err.out_of_range.v1~"); const RESOURCE_EXHAUSTED_TYPE: &str = - "gts://gts.cf.core.errors.err.v1~cf.core.err.resource_exhausted.v1~"; + gts_uri!("cf.core.errors.err.v1~cf.core.err.resource_exhausted.v1~"); const FAILED_PRECONDITION_TYPE: &str = - "gts://gts.cf.core.errors.err.v1~cf.core.err.failed_precondition.v1~"; + gts_uri!("cf.core.errors.err.v1~cf.core.err.failed_precondition.v1~"); const PERMISSION_DENIED_TYPE: &str = - "gts://gts.cf.core.errors.err.v1~cf.core.err.permission_denied.v1~"; - const ABORTED_TYPE: &str = "gts://gts.cf.core.errors.err.v1~cf.core.err.aborted.v1~"; + gts_uri!("cf.core.errors.err.v1~cf.core.err.permission_denied.v1~"); + const ABORTED_TYPE: &str = gts_uri!("cf.core.errors.err.v1~cf.core.err.aborted.v1~"); const SERVICE_UNAVAILABLE_TYPE: &str = - "gts://gts.cf.core.errors.err.v1~cf.core.err.service_unavailable.v1~"; + gts_uri!("cf.core.errors.err.v1~cf.core.err.service_unavailable.v1~"); - const CHAT_GTS: &str = "gts.cf.core.mini_chat.chat.v1~"; - const MESSAGE_GTS: &str = "gts.cf.core.mini_chat.message.v1~"; - const TURN_GTS: &str = "gts.cf.core.mini_chat.turn.v1~"; - const ATTACHMENT_GTS: &str = "gts.cf.core.mini_chat.attachment.v1~"; - const MODEL_GTS: &str = "gts.cf.core.mini_chat.model.v1~"; + const CHAT_GTS: &str = gts_id!("cf.core.mini_chat.chat.v1~"); + const MESSAGE_GTS: &str = gts_id!("cf.core.mini_chat.message.v1~"); + const TURN_GTS: &str = gts_id!("cf.core.mini_chat.turn.v1~"); + const ATTACHMENT_GTS: &str = gts_id!("cf.core.mini_chat.attachment.v1~"); + const MODEL_GTS: &str = gts_id!("cf.core.mini_chat.model.v1~"); // ── Resource scope coverage (one not_found per scope) ──────────────── diff --git a/gears/mini-chat/mini-chat/src/api/rest/routes/mod.rs b/gears/mini-chat/mini-chat/src/api/rest/routes/mod.rs index 0b6616d6d..e96cbe71e 100644 --- a/gears/mini-chat/mini-chat/src/api/rest/routes/mod.rs +++ b/gears/mini-chat/mini-chat/src/api/rest/routes/mod.rs @@ -10,6 +10,7 @@ use std::sync::Arc; use axum::Router; use toolkit::api::OpenApiRegistry; +use toolkit::api::operation_builder::CORE_GLOBAL_BASE_LICENSE_FEATURE; use toolkit::api::operation_builder::LicenseFeature; use crate::gear::AppServices; @@ -25,7 +26,7 @@ pub(crate) struct AiChatLicense; // once the license plugin can provide necessary information. impl AsRef for AiChatLicense { fn as_ref(&self) -> &'static str { - "gts.cf.core.lic.feat.v1~cf.core.global.base.v1" + CORE_GLOBAL_BASE_LICENSE_FEATURE } } diff --git a/gears/mini-chat/mini-chat/src/config.rs b/gears/mini-chat/mini-chat/src/config.rs index d849d65dc..81809f12c 100644 --- a/gears/mini-chat/mini-chat/src/config.rs +++ b/gears/mini-chat/mini-chat/src/config.rs @@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize}; use crate::gear::DEFAULT_URL_PREFIX; use crate::infra::llm::ProviderKind; +use oagw_sdk::APIKEY_AUTH_PLUGIN_ID; pub mod background; pub use background::{CleanupWorkerConfig, OrphanWatchdogConfig, ThreadSummaryWorkerConfig}; @@ -310,9 +311,7 @@ fn default_providers() -> HashMap { port: None, use_http: false, api_path: default_api_path(), - auth_plugin_type: Some( - "gts.cf.core.oagw.auth_plugin.v1~cf.core.oagw.apikey.v1".to_owned(), - ), + auth_plugin_type: Some(APIKEY_AUTH_PLUGIN_ID.to_owned()), auth_config: Some({ let mut c = HashMap::new(); c.insert("header".to_owned(), "Authorization".to_owned()); @@ -1292,18 +1291,18 @@ mod tests { #[test] fn provider_entry_deser_with_auth() { - let json = r#"{ + let json = serde_json::json!({ "kind": "openai_responses", "storage_kind": "openai", "host": "api.openai.com", - "auth_plugin_type": "gts.cf.core.oagw.auth_plugin.v1~cf.core.oagw.apikey.v1", + "auth_plugin_type": APIKEY_AUTH_PLUGIN_ID, "auth_config": { "header": "Authorization", "prefix": "Bearer ", "secret_ref": "cred://openai-key" } - }"#; - let entry: ProviderEntry = serde_json::from_str(json).unwrap(); + }); + let entry: ProviderEntry = serde_json::from_value(json).unwrap(); assert!(entry.auth_plugin_type.is_some()); let config = entry.auth_config.unwrap(); assert_eq!(config.get("header").unwrap(), "Authorization"); diff --git a/gears/mini-chat/mini-chat/src/domain/service/attachment_service_test.rs b/gears/mini-chat/mini-chat/src/domain/service/attachment_service_test.rs index 744cf0a3f..7c57097ea 100644 --- a/gears/mini-chat/mini-chat/src/domain/service/attachment_service_test.rs +++ b/gears/mini-chat/mini-chat/src/domain/service/attachment_service_test.rs @@ -8,7 +8,7 @@ use uuid::Uuid; /// Local resource scope for fabricating canonical errors that mirror what /// the OAGW impl crate emits at runtime — used by the `MockOagwGateway` /// fixtures below. -#[resource_error("gts.cf.core.oagw.proxy.v1~")] +#[resource_error(gts_id!("cf.core.oagw.proxy.v1~"))] struct TestProxyScope; fn timeout_error(detail: &str) -> CanonicalError { diff --git a/gears/mini-chat/mini-chat/src/domain/service/mod.rs b/gears/mini-chat/mini-chat/src/domain/service/mod.rs index 62d70c10e..8b228f179 100644 --- a/gears/mini-chat/mini-chat/src/domain/service/mod.rs +++ b/gears/mini-chat/mini-chat/src/domain/service/mod.rs @@ -74,10 +74,11 @@ pub(crate) type DbProvider = DBProvider; #[allow(dead_code)] pub(crate) mod resources { use super::ResourceType; + use mini_chat_sdk::{CHAT_RESOURCE_TYPE, MODEL_RESOURCE_TYPE, USER_QUOTA_RESOURCE_TYPE}; use toolkit_security::pep_properties; pub const CHAT: ResourceType = ResourceType::from_static( - "gts.cf.core.ai_chat.chat.v1~cf.core.mini_chat.chat.v1~", + CHAT_RESOURCE_TYPE, &[ pep_properties::OWNER_TENANT_ID, pep_properties::OWNER_ID, @@ -85,13 +86,11 @@ pub(crate) mod resources { ], ); - pub const MODEL: ResourceType = ResourceType::from_static( - "gts.cf.core.ai_chat.model.v1~cf.core.mini_chat.model.v1~", - &[pep_properties::OWNER_TENANT_ID], - ); + pub const MODEL: ResourceType = + ResourceType::from_static(MODEL_RESOURCE_TYPE, &[pep_properties::OWNER_TENANT_ID]); pub const USER_QUOTA: ResourceType = ResourceType::from_static( - "gts.cf.core.ai_chat.user_quota.v1~cf.core.mini_chat.user_quota.v1~", + USER_QUOTA_RESOURCE_TYPE, &[pep_properties::OWNER_TENANT_ID, pep_properties::OWNER_ID], ); } diff --git a/gears/mini-chat/mini-chat/src/gts/permissions.rs b/gears/mini-chat/mini-chat/src/gts/permissions.rs index c1d81654c..e6a8a981b 100644 --- a/gears/mini-chat/mini-chat/src/gts/permissions.rs +++ b/gears/mini-chat/mini-chat/src/gts/permissions.rs @@ -36,21 +36,23 @@ //! [`gts_instance!`]: toolkit_gts::gts_instance use crate::domain::service::actions; -use toolkit_gts::{AuthzPermissionV1, gts_instance}; +use toolkit_gts::{AuthzPermissionV1, gts_id, gts_instance}; /// Wildcard `resource_type` for permissions over any mini-chat chat. /// /// Covers `gts.cf.core.ai_chat.chat.v1~cf.core.mini_chat.chat.v1~` (the /// concrete type the PEP sends) plus any future derivation under that /// subtree. -const CHAT_RESOURCE_TYPE_WILDCARD: &str = "gts.cf.core.ai_chat.chat.v1~cf.core.mini_chat.chat.*"; +const CHAT_RESOURCE_TYPE_WILDCARD: &str = + gts_id!("cf.core.ai_chat.chat.v1~cf.core.mini_chat.chat.*"); /// Wildcard `resource_type` for permissions over any mini-chat model. -const MODEL_RESOURCE_TYPE_WILDCARD: &str = "gts.cf.core.ai_chat.model.v1~cf.core.mini_chat.model.*"; +const MODEL_RESOURCE_TYPE_WILDCARD: &str = + gts_id!("cf.core.ai_chat.model.v1~cf.core.mini_chat.model.*"); /// Wildcard `resource_type` for permissions over any mini-chat user-quota. const USER_QUOTA_RESOURCE_TYPE_WILDCARD: &str = - "gts.cf.core.ai_chat.user_quota.v1~cf.core.mini_chat.user_quota.*"; + gts_id!("cf.core.ai_chat.user_quota.v1~cf.core.mini_chat.user_quota.*"); // ===================================================================== // CHAT resource permissions @@ -59,7 +61,7 @@ const USER_QUOTA_RESOURCE_TYPE_WILDCARD: &str = gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_create.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_create.v1"), resource_type: CHAT_RESOURCE_TYPE_WILDCARD.to_owned(), action: actions::CREATE.to_owned(), display_name: "Create chat".to_owned(), } @@ -67,7 +69,7 @@ gts_instance! { gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_read.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_read.v1"), resource_type: CHAT_RESOURCE_TYPE_WILDCARD.to_owned(), action: actions::READ.to_owned(), display_name: "Read chat".to_owned(), } @@ -75,7 +77,7 @@ gts_instance! { gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_list.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_list.v1"), resource_type: CHAT_RESOURCE_TYPE_WILDCARD.to_owned(), action: actions::LIST.to_owned(), display_name: "List chats".to_owned(), } @@ -83,7 +85,7 @@ gts_instance! { gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_update.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_update.v1"), resource_type: CHAT_RESOURCE_TYPE_WILDCARD.to_owned(), action: actions::UPDATE.to_owned(), display_name: "Update chat".to_owned(), } @@ -91,7 +93,7 @@ gts_instance! { gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_delete.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_delete.v1"), resource_type: CHAT_RESOURCE_TYPE_WILDCARD.to_owned(), action: actions::DELETE.to_owned(), display_name: "Delete chat".to_owned(), } @@ -99,7 +101,7 @@ gts_instance! { gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_list_messages.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_list_messages.v1"), resource_type: CHAT_RESOURCE_TYPE_WILDCARD.to_owned(), action: actions::LIST_MESSAGES.to_owned(), display_name: "List chat messages".to_owned(), } @@ -107,7 +109,7 @@ gts_instance! { gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_send_message.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_send_message.v1"), resource_type: CHAT_RESOURCE_TYPE_WILDCARD.to_owned(), action: actions::SEND_MESSAGE.to_owned(), display_name: "Send chat message".to_owned(), } @@ -115,7 +117,7 @@ gts_instance! { gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_read_turn.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_read_turn.v1"), resource_type: CHAT_RESOURCE_TYPE_WILDCARD.to_owned(), action: actions::READ_TURN.to_owned(), display_name: "Read chat turn".to_owned(), } @@ -123,7 +125,7 @@ gts_instance! { gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_retry_turn.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_retry_turn.v1"), resource_type: CHAT_RESOURCE_TYPE_WILDCARD.to_owned(), action: actions::RETRY_TURN.to_owned(), display_name: "Retry chat turn".to_owned(), } @@ -131,7 +133,7 @@ gts_instance! { gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_edit_turn.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_edit_turn.v1"), resource_type: CHAT_RESOURCE_TYPE_WILDCARD.to_owned(), action: actions::EDIT_TURN.to_owned(), display_name: "Edit chat turn".to_owned(), } @@ -139,7 +141,7 @@ gts_instance! { gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_delete_turn.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_delete_turn.v1"), resource_type: CHAT_RESOURCE_TYPE_WILDCARD.to_owned(), action: actions::DELETE_TURN.to_owned(), display_name: "Delete chat turn".to_owned(), } @@ -147,7 +149,7 @@ gts_instance! { gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_upload_attachment.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_upload_attachment.v1"), resource_type: CHAT_RESOURCE_TYPE_WILDCARD.to_owned(), action: actions::UPLOAD_ATTACHMENT.to_owned(), display_name: "Upload attachment".to_owned(), } @@ -155,7 +157,7 @@ gts_instance! { gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_read_attachment.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_read_attachment.v1"), resource_type: CHAT_RESOURCE_TYPE_WILDCARD.to_owned(), action: actions::READ_ATTACHMENT.to_owned(), display_name: "Read attachment".to_owned(), } @@ -163,7 +165,7 @@ gts_instance! { gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_delete_attachment.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_delete_attachment.v1"), resource_type: CHAT_RESOURCE_TYPE_WILDCARD.to_owned(), action: actions::DELETE_ATTACHMENT.to_owned(), display_name: "Delete attachment".to_owned(), } @@ -171,7 +173,7 @@ gts_instance! { gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_set_reaction.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_set_reaction.v1"), resource_type: CHAT_RESOURCE_TYPE_WILDCARD.to_owned(), action: actions::SET_REACTION.to_owned(), display_name: "Set reaction".to_owned(), } @@ -179,7 +181,7 @@ gts_instance! { gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_delete_reaction.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_delete_reaction.v1"), resource_type: CHAT_RESOURCE_TYPE_WILDCARD.to_owned(), action: actions::DELETE_REACTION.to_owned(), display_name: "Delete reaction".to_owned(), } @@ -192,7 +194,7 @@ gts_instance! { gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.mini_chat._.model_list.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.mini_chat._.model_list.v1"), resource_type: MODEL_RESOURCE_TYPE_WILDCARD.to_owned(), action: actions::LIST.to_owned(), display_name: "List models".to_owned(), } @@ -200,7 +202,7 @@ gts_instance! { gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.mini_chat._.model_read.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.mini_chat._.model_read.v1"), resource_type: MODEL_RESOURCE_TYPE_WILDCARD.to_owned(), action: actions::READ.to_owned(), display_name: "Read model".to_owned(), } @@ -213,7 +215,7 @@ gts_instance! { gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.mini_chat._.user_quota_read.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.mini_chat._.user_quota_read.v1"), resource_type: USER_QUOTA_RESOURCE_TYPE_WILDCARD.to_owned(), action: actions::READ.to_owned(), display_name: "Read user quota".to_owned(), } @@ -226,33 +228,36 @@ mod tests { USER_QUOTA_RESOURCE_TYPE_WILDCARD, actions, }; use crate::domain::service::resources; - use toolkit_gts::InventoryInstance; + use toolkit_gts::{InventoryInstance, gts_id}; #[allow(unknown_lints, de0901_gts_string_pattern)] - const INSTANCE_PREFIX: &str = "gts.cf.toolkit.authz.permission.v1~cf.mini_chat._."; + const INSTANCE_PREFIX: &str = concat!( + gts_id!("cf.toolkit.authz.permission.v1~"), + "cf.mini_chat._." + ); /// Expected set of permission instance ids for mini-chat. One entry per /// `(resource, action)` tuple the gear exposes at PEP call-sites. const EXPECTED_PERMISSION_IDS: &[&str] = &[ - "gts.cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_create.v1", - "gts.cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_read.v1", - "gts.cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_list.v1", - "gts.cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_update.v1", - "gts.cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_delete.v1", - "gts.cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_list_messages.v1", - "gts.cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_send_message.v1", - "gts.cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_read_turn.v1", - "gts.cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_retry_turn.v1", - "gts.cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_edit_turn.v1", - "gts.cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_delete_turn.v1", - "gts.cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_upload_attachment.v1", - "gts.cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_read_attachment.v1", - "gts.cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_delete_attachment.v1", - "gts.cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_set_reaction.v1", - "gts.cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_delete_reaction.v1", - "gts.cf.toolkit.authz.permission.v1~cf.mini_chat._.model_list.v1", - "gts.cf.toolkit.authz.permission.v1~cf.mini_chat._.model_read.v1", - "gts.cf.toolkit.authz.permission.v1~cf.mini_chat._.user_quota_read.v1", + gts_id!("cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_create.v1"), + gts_id!("cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_read.v1"), + gts_id!("cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_list.v1"), + gts_id!("cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_update.v1"), + gts_id!("cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_delete.v1"), + gts_id!("cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_list_messages.v1"), + gts_id!("cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_send_message.v1"), + gts_id!("cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_read_turn.v1"), + gts_id!("cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_retry_turn.v1"), + gts_id!("cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_edit_turn.v1"), + gts_id!("cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_delete_turn.v1"), + gts_id!("cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_upload_attachment.v1"), + gts_id!("cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_read_attachment.v1"), + gts_id!("cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_delete_attachment.v1"), + gts_id!("cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_set_reaction.v1"), + gts_id!("cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_delete_reaction.v1"), + gts_id!("cf.toolkit.authz.permission.v1~cf.mini_chat._.model_list.v1"), + gts_id!("cf.toolkit.authz.permission.v1~cf.mini_chat._.model_read.v1"), + gts_id!("cf.toolkit.authz.permission.v1~cf.mini_chat._.user_quota_read.v1"), ]; fn mini_chat_permission_instances() -> Vec<&'static InventoryInstance> { diff --git a/gears/mini-chat/mini-chat/src/infra/llm/providers/openai_responses_tests.rs b/gears/mini-chat/mini-chat/src/infra/llm/providers/openai_responses_tests.rs index 3ceed61e1..a044ae056 100644 --- a/gears/mini-chat/mini-chat/src/infra/llm/providers/openai_responses_tests.rs +++ b/gears/mini-chat/mini-chat/src/infra/llm/providers/openai_responses_tests.rs @@ -1406,7 +1406,7 @@ async fn cancellation_terminates_stream() { // ── Integration test: OAGW error paths ───────────────────────────────── -#[resource_error("gts.cf.core.oagw.proxy.v1~")] +#[resource_error(gts_id!("cf.core.oagw.proxy.v1~"))] struct TestProxyScope; #[tokio::test] diff --git a/gears/mini-chat/mini-chat/src/infra/oagw_provisioning.rs b/gears/mini-chat/mini-chat/src/infra/oagw_provisioning.rs index 44dd40651..1cea0dddb 100644 --- a/gears/mini-chat/mini-chat/src/infra/oagw_provisioning.rs +++ b/gears/mini-chat/mini-chat/src/infra/oagw_provisioning.rs @@ -11,6 +11,7 @@ use std::collections::HashMap; use std::sync::Arc; +use oagw_sdk::HTTP_PROTOCOL_ID; use oagw_sdk::ServiceGatewayClientV1; use tracing::{info, warn}; @@ -107,9 +108,7 @@ async fn create_upstream( endpoints: vec![endpoint_for(entry)], }; - let mut builder = - CreateUpstreamRequest::builder(server, "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1") - .enabled(true); + let mut builder = CreateUpstreamRequest::builder(server, HTTP_PROTOCOL_ID).enabled(true); // Only pass alias when explicitly configured (IP-based hosts). if let Some(alias) = &entry.upstream_alias { @@ -176,9 +175,7 @@ async fn create_tenant_upstream( endpoints: vec![ep], }; - let mut builder = - CreateUpstreamRequest::builder(server, "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1") - .enabled(true); + let mut builder = CreateUpstreamRequest::builder(server, HTTP_PROTOCOL_ID).enabled(true); // Only pass alias when the tenant override explicitly sets one (IP-based hosts). if let Some(alias) = entry diff --git a/gears/model-registry/model-registry-sdk/Cargo.toml b/gears/model-registry/model-registry-sdk/Cargo.toml index a5f835eae..13830c9d2 100644 --- a/gears/model-registry/model-registry-sdk/Cargo.toml +++ b/gears/model-registry/model-registry-sdk/Cargo.toml @@ -26,5 +26,5 @@ serde_json = { workspace = true } schemars = { workspace = true, features = ["chrono04"] } toolkit-security = { workspace = true } toolkit-odata = { workspace = true } +toolkit-gts = { workspace = true } gts = { workspace = true } -gts-macros = { workspace = true } \ No newline at end of file diff --git a/gears/model-registry/model-registry-sdk/src/models/entity.rs b/gears/model-registry/model-registry-sdk/src/models/entity.rs index 79b39a877..9f383f561 100644 --- a/gears/model-registry/model-registry-sdk/src/models/entity.rs +++ b/gears/model-registry/model-registry-sdk/src/models/entity.rs @@ -136,6 +136,7 @@ pub struct ProviderV1 { mod tests { use super::*; use std::collections::{HashMap, HashSet}; + use toolkit_gts::gts_id; use gts::GtsSchema; @@ -289,7 +290,7 @@ mod tests { // caller asks for OpenAi — typed-narrowing must surface a SchemaId // error rather than silently deserializing the wrong shape. let m = raw_model( - "gts.cf.genai.model.info.v1~cf.genai._.anthropic.v1~", + gts_id!("cf.genai.model.info.v1~cf.genai._.anthropic.v1~"), openai_payload(), ); let err = m @@ -300,7 +301,7 @@ mod tests { assert_eq!(expected, OpenAiSettingsV1::TYPE_ID); assert_eq!( actual, - "gts.cf.genai.model.info.v1~cf.genai._.anthropic.v1~" + gts_id!("cf.genai.model.info.v1~cf.genai._.anthropic.v1~") ); } other @ gts::NarrowError::Deserialize(_) => { @@ -313,7 +314,7 @@ mod tests { fn try_into_typed_fails_on_unknown_gts_type() { // Unknown / unmodeled provider — base envelope schema with no leaf. let m = raw_model( - "gts.cf.genai.model.info.v1~", + gts_id!("cf.genai.model.info.v1~"), serde_json::json!({ "anything": "goes" }), ); let err = m diff --git a/gears/model-registry/model-registry-sdk/src/models/info.rs b/gears/model-registry/model-registry-sdk/src/models/info.rs index 58932668a..4fcfd7e20 100644 --- a/gears/model-registry/model-registry-sdk/src/models/info.rs +++ b/gears/model-registry/model-registry-sdk/src/models/info.rs @@ -5,7 +5,7 @@ //! (`allow_parameter_override`, `allow_extra_params`), and the typed //! `provider_settings: P` payload. //! -//! Declared as the GTS base schema via [`struct_to_gts_schema`]; concrete +//! Declared as the GTS base schema via [`gts_type_schema`]; concrete //! provider-settings types (e.g. `OpenAiSettingsV1`, shipped in //! [`crate::models::providers`]) declare themselves as GTS leaves with //! `base = ModelInfoV1`. The set of provider leaves is open-ended. @@ -18,7 +18,7 @@ use std::collections::{HashMap, HashSet}; use chrono::{DateTime, Utc}; -use gts_macros::struct_to_gts_schema; +use toolkit_gts::gts_type_schema; use crate::models::{ ContextWindow, DefaultInferenceParametersV1, DisabledCapabilities, ModelCapabilities, @@ -37,12 +37,12 @@ use crate::models::{ /// /// # GTS schema /// -/// - **`schema_id`**: `gts.cf.genai.model.info.v1~` +/// - **`schema_id`**: `gts_id!("cf.genai.model.info.v1~")` /// - **base**: yes (root envelope; provider-specific leaves chain off it) -#[struct_to_gts_schema( +#[gts_type_schema( dir_path = "schemas", base = true, - type_id = "gts.cf.genai.model.info.v1~", + type_id = gts_id!("cf.genai.model.info.v1~"), description = "Generic model info envelope: provider-independent metadata + provider_settings JSON payload", properties = "gts_type,display_name,family,vendor,managed,architecture,format,supported_api,provider_model_id,capabilities,context_window,default_parameters" )] @@ -50,8 +50,7 @@ use crate::models::{ #[non_exhaustive] pub struct ModelInfoV1 { // ── GTS schema identity ─────────────────────────────────────────── - /// Full GTS schema chain identifying this model's settings shape — e.g. - /// `gts.cf.genai.model.info.v1~cf.genai._.openai.v1~`. Mirrors + /// Full GTS schema chain identifying this model's settings shape. Mirrors /// `ProviderV1.gts_type` and is the canonical key for resolving the /// concrete shape of `provider_settings` (which is a raw JSON blob — /// `serde_json::Value` — by default). diff --git a/gears/model-registry/model-registry-sdk/src/models/providers/anthropic.rs b/gears/model-registry/model-registry-sdk/src/models/providers/anthropic.rs index 72a2ce740..c63d60349 100644 --- a/gears/model-registry/model-registry-sdk/src/models/providers/anthropic.rs +++ b/gears/model-registry/model-registry-sdk/src/models/providers/anthropic.rs @@ -6,7 +6,7 @@ //! [`AnthropicSettingsV1`]. The per-model override policy //! (`allow_parameter_override`, `allow_extra_params`) is **not** here — //! those are flat fields on [`crate::models::ModelInfoV1`]. Declared as a -//! GTS schema leaf via [`struct_to_gts_schema`]; its parent envelope is +//! GTS schema leaf via [`gts_type_schema`]; its parent envelope is //! `ModelInfoV1

`. //! //! Per-request fields (`messages`, `model`, `tools`, `metadata`, @@ -16,7 +16,7 @@ //! Note: `supported_api` and `provider_model_id` live on `ModelInfoV1` //! (common), not on `AnthropicSettingsV1`. -use gts_macros::struct_to_gts_schema; +use toolkit_gts::gts_type_schema; use crate::models::ModelInfoV1; @@ -212,12 +212,12 @@ pub struct AnthropicCost { /// /// # GTS schema /// -/// - **`schema_id`**: `gts.cf.genai.model.info.v1~cf.genai._.anthropic.v1~` +/// - **`schema_id`**: `gts_id!("cf.genai.model.info.v1~cf.genai._.anthropic.v1~")` /// - **base**: `ModelInfoV1` (the generic envelope) -#[struct_to_gts_schema( +#[gts_type_schema( dir_path = "schemas", base = ModelInfoV1, - type_id = "gts.cf.genai.model.info.v1~cf.genai._.anthropic.v1~", + type_id = gts_id!("cf.genai.model.info.v1~cf.genai._.anthropic.v1~"), description = "Anthropic provider settings (Messages API)", properties = "oagw_alias,anthropic_version,anthropic_beta,temperature,top_p,top_k,max_tokens,stop_sequences,system,inference_geo,service_tier,container,thinking,tool_choice,output_config,cost" )] diff --git a/gears/model-registry/model-registry-sdk/src/models/providers/mod.rs b/gears/model-registry/model-registry-sdk/src/models/providers/mod.rs index 2d3d89594..2c332115f 100644 --- a/gears/model-registry/model-registry-sdk/src/models/providers/mod.rs +++ b/gears/model-registry/model-registry-sdk/src/models/providers/mod.rs @@ -26,13 +26,13 @@ //! cost struct, and any provider-specific helper enums (response format //! variants, tool-choice shapes, etc.). //! -//! 2. Decorate the aggregate with [`gts_macros::struct_to_gts_schema`]: +//! 2. Decorate the aggregate with [`toolkit_gts::gts_type_schema`]: //! //! ```ignore -//! #[struct_to_gts_schema( +//! #[gts_type_schema( //! dir_path = "schemas", //! base = ModelInfoV1, -//! type_id = "gts.cf.genai.model.info.v1~...v~", +//! type_id = gts_id!("cf.genai.model.info.v1~...v~"), //! description = " provider settings", //! properties = "" //! )] diff --git a/gears/model-registry/model-registry-sdk/src/models/providers/openai.rs b/gears/model-registry/model-registry-sdk/src/models/providers/openai.rs index 249b52c1b..9634b905a 100644 --- a/gears/model-registry/model-registry-sdk/src/models/providers/openai.rs +++ b/gears/model-registry/model-registry-sdk/src/models/providers/openai.rs @@ -8,7 +8,7 @@ //! per-model override policy (`allow_parameter_override`, //! `allow_extra_params`) is **not** here — those are flat fields on //! [`crate::models::ModelInfoV1`]. Declared as a GTS schema leaf via -//! [`struct_to_gts_schema`]; its parent envelope is `ModelInfoV1

`. +//! [`gts_type_schema`]; its parent envelope is `ModelInfoV1

`. //! //! Field set is verified against the `OpenAPI` spec for `POST //! /v1/chat/completions`, `POST /v1/responses`, and `POST /v1/embeddings`. @@ -22,7 +22,7 @@ //! Note: `supported_api` and `provider_model_id` live on `ModelInfoV1` //! (common), not on `OpenAiSettingsV1`. -use gts_macros::struct_to_gts_schema; +use toolkit_gts::gts_type_schema; use crate::models::{ModelInfoV1, ReasoningSummary, TextVerbosity, TruncationStrategy}; @@ -221,12 +221,12 @@ pub struct OpenAiCost { /// /// # GTS schema /// -/// - **`schema_id`**: `gts.cf.genai.model.info.v1~cf.genai._.openai.v1~` +/// - **`schema_id`**: `gts_id!("cf.genai.model.info.v1~cf.genai._.openai.v1~")` /// - **base**: `ModelInfoV1` (the generic envelope) -#[struct_to_gts_schema( +#[gts_type_schema( dir_path = "schemas", base = ModelInfoV1, - type_id = "gts.cf.genai.model.info.v1~cf.genai._.openai.v1~", + type_id = gts_id!("cf.genai.model.info.v1~cf.genai._.openai.v1~"), description = "OpenAI provider settings (Chat Completions / Responses / Embeddings)", properties = "oagw_alias,endpoint_kind,organization,project,temperature,top_p,presence_penalty,frequency_penalty,top_logprobs,service_tier,prompt_cache_retention,reasoning_effort,reasoning_summary,verbosity,parallel_tool_calls,store,response_format,max_tokens,max_completion_tokens,n,stop,seed,logprobs,max_output_tokens,max_tool_calls,truncation,encoding_format,dimensions,cost" )] diff --git a/gears/model-registry/model-registry-sdk/src/models/request.rs b/gears/model-registry/model-registry-sdk/src/models/request.rs index 4dd106016..00636bcf5 100644 --- a/gears/model-registry/model-registry-sdk/src/models/request.rs +++ b/gears/model-registry/model-registry-sdk/src/models/request.rs @@ -285,10 +285,11 @@ pub struct UpdateModelRequestV1 { #[cfg(test)] mod tests { use super::*; + use toolkit_gts::gts_id; #[test] fn provider_builder_threads_required_fields_and_applies_overrides() { - let gts = gts::GtsTypeId::new("gts.cf.genai.models.provider.v1~cf.genai._.openai.v1~"); + let gts = gts::GtsTypeId::new(gts_id!("cf.genai.models.provider.v1~cf.genai._.openai.v1~")); let full = CreateProviderRequestV1::builder("openai", "OpenAI", gts.clone()) .managed(true) diff --git a/gears/simple-user-settings/simple-user-settings/Cargo.toml b/gears/simple-user-settings/simple-user-settings/Cargo.toml index 7d08a1fb9..47bf46f51 100644 --- a/gears/simple-user-settings/simple-user-settings/Cargo.toml +++ b/gears/simple-user-settings/simple-user-settings/Cargo.toml @@ -44,3 +44,6 @@ toolkit-db-macros = { workspace = true } toolkit-canonical-errors = { workspace = true, features = ["axum"] } toolkit-security = { workspace = true } toolkit-macros = { workspace = true } + +[dev-dependencies] +toolkit-gts = { workspace = true } diff --git a/gears/simple-user-settings/simple-user-settings/src/api/rest/error.rs b/gears/simple-user-settings/simple-user-settings/src/api/rest/error.rs index 80a075983..ebcd06961 100644 --- a/gears/simple-user-settings/simple-user-settings/src/api/rest/error.rs +++ b/gears/simple-user-settings/simple-user-settings/src/api/rest/error.rs @@ -2,7 +2,7 @@ use toolkit_canonical_errors::{CanonicalError, resource_error}; use crate::domain::error::DomainError; -#[resource_error("gts.cf.simple_user_settings.settings.user.v1~")] +#[resource_error(gts_id!("cf.simple_user_settings.settings.user.v1~"))] pub struct UserSettingsError; impl From for CanonicalError { diff --git a/gears/simple-user-settings/simple-user-settings/src/api/rest/error_test.rs b/gears/simple-user-settings/simple-user-settings/src/api/rest/error_test.rs index d046fba38..c8fbd6cd9 100644 --- a/gears/simple-user-settings/simple-user-settings/src/api/rest/error_test.rs +++ b/gears/simple-user-settings/simple-user-settings/src/api/rest/error_test.rs @@ -2,6 +2,9 @@ mod tests { use crate::domain::error::DomainError; use toolkit_canonical_errors::{CanonicalError, Problem}; + use toolkit_gts::gts_id; + + const SETTINGS_RESOURCE_TYPE: &str = gts_id!("cf.simple_user_settings.settings.user.v1~"); /// Build the wire `Problem` the canonical error middleware would emit /// for a given `DomainError`. Tests run without the middleware in @@ -23,7 +26,7 @@ mod tests { .context .get("resource_type") .and_then(|v| v.as_str()), - Some("gts.cf.simple_user_settings.settings.user.v1~"), + Some(SETTINGS_RESOURCE_TYPE), ); } @@ -84,7 +87,7 @@ mod tests { .context .get("resource_type") .and_then(|v| v.as_str()), - Some("gts.cf.simple_user_settings.settings.user.v1~"), + Some(SETTINGS_RESOURCE_TYPE), ); } diff --git a/gears/simple-user-settings/simple-user-settings/src/api/rest/routes.rs b/gears/simple-user-settings/simple-user-settings/src/api/rest/routes.rs index cb2d79781..a47970573 100644 --- a/gears/simple-user-settings/simple-user-settings/src/api/rest/routes.rs +++ b/gears/simple-user-settings/simple-user-settings/src/api/rest/routes.rs @@ -4,7 +4,7 @@ use crate::infra::storage::sea_orm_repo::SeaOrmSettingsRepository; use axum::http::StatusCode; use axum::{Extension, Router}; use std::sync::Arc; -use toolkit::api::operation_builder::LicenseFeature; +use toolkit::api::operation_builder::{CORE_GLOBAL_BASE_LICENSE_FEATURE, LicenseFeature}; use toolkit::api::{OpenApiRegistry, OperationBuilder}; /// Type alias for the concrete service type. @@ -14,7 +14,7 @@ struct License; impl AsRef for License { fn as_ref(&self) -> &'static str { - "gts.cf.core.lic.feat.v1~cf.core.global.base.v1" + CORE_GLOBAL_BASE_LICENSE_FEATURE } } @@ -97,10 +97,7 @@ mod tests { #[test] fn test_license_as_ref() { let license = License; - assert_eq!( - license.as_ref(), - "gts.cf.core.lic.feat.v1~cf.core.global.base.v1" - ); + assert_eq!(license.as_ref(), CORE_GLOBAL_BASE_LICENSE_FEATURE); } #[test] diff --git a/gears/system/account-management/account-management-sdk/Cargo.toml b/gears/system/account-management/account-management-sdk/Cargo.toml index 5c95e52ab..4f458bf36 100644 --- a/gears/system/account-management/account-management-sdk/Cargo.toml +++ b/gears/system/account-management/account-management-sdk/Cargo.toml @@ -79,7 +79,6 @@ toolkit-security = { workspace = true } # `toolkit` ships `PluginV1

` (re-exported from `toolkit-gts`); # `schemars` is the runtime JSON-Schema reflection backend the # macro generates code against. -gts-macros = { workspace = true } toolkit = { workspace = true } toolkit-gts = { workspace = true } schemars = { workspace = true } diff --git a/gears/system/account-management/account-management-sdk/src/error_tests.rs b/gears/system/account-management/account-management-sdk/src/error_tests.rs index 8cbc2175c..850243d09 100644 --- a/gears/system/account-management/account-management-sdk/src/error_tests.rs +++ b/gears/system/account-management/account-management-sdk/src/error_tests.rs @@ -25,16 +25,16 @@ mod wire_vocabulary_round_trip { // Test scopes mirroring the impl crate's `#[resource_error]` markers. // Their literals MUST equal the `gts::*` constants — the // `gts_resource_types_round_trip` test asserts that equality. - #[resource_error("gts.cf.core.am.tenant.v1~")] + #[resource_error(gts_id!("cf.core.am.tenant.v1~"))] struct TenantScope; - #[resource_error("gts.cf.core.am.user.v1~")] + #[resource_error(gts_id!("cf.core.am.user.v1~"))] struct UserScope; - #[resource_error("gts.cf.core.am.tenant_metadata.v1~")] + #[resource_error(gts_id!("cf.core.am.tenant_metadata.v1~"))] struct MetadataScope; - #[resource_error("gts.cf.core.am.conversion_request.v1~")] + #[resource_error(gts_id!("cf.core.am.conversion_request.v1~"))] struct ConversionScope; fn problem(err: CanonicalError) -> serde_json::Value { @@ -203,10 +203,10 @@ mod projection_tests { use crate::{field, gts, precondition, quota, reason}; use toolkit_canonical_errors::{CanonicalError, Problem, resource_error}; - #[resource_error("gts.cf.core.am.tenant.v1~")] + #[resource_error(gts_id!("cf.core.am.tenant.v1~"))] struct TenantScope; - #[resource_error("gts.cf.core.am.tenant_metadata.v1~")] + #[resource_error(gts_id!("cf.core.am.tenant_metadata.v1~"))] struct MetadataScope; #[test] diff --git a/gears/system/account-management/account-management-sdk/src/gts.rs b/gears/system/account-management/account-management-sdk/src/gts.rs index 4356c9b8c..fdc75d137 100644 --- a/gears/system/account-management/account-management-sdk/src/gts.rs +++ b/gears/system/account-management/account-management-sdk/src/gts.rs @@ -13,26 +13,20 @@ //! build graphs slim. //! //! Strings follow the AM-specific GTS namespace convention from -//! `gears/system/account-management/docs/DESIGN.md` (PEP table): -//! `gts.cf.core.am.{resource}.v1~`. The trailing `~` is the GTS -//! terminator and is part of the identifier. +//! `gears/system/account-management/docs/DESIGN.md` (PEP table). //! //! Mirrors the `gts` gear layout used by `resource-group-sdk` — //! see `account_management_sdk::lib` rationale for the SDK split. //! //! # Note on `#[resource_error]` macro arguments //! -//! The `toolkit_canonical_errors::resource_error` proc-macro takes a -//! literal string at expansion time and cannot resolve constants — -//! the impl-crate sites that call the macro therefore duplicate -//! these literals. The `domain::error_tests` gear asserts the -//! impl-crate strings match the constants below, so a divergence -//! trips at test time, not in production. +//! Use the marker form `#[resource_error(gts_id!("..."))]` in impl +//! crates so IDs flow through the same toolkit GTS macro path. /// AM Tenant resource. Used for PEP authorization on the `tenants` /// table and as the `resource_type` field on tenant-scoped canonical /// errors (e.g. `tenant {id} not found` → 404). -pub const TENANT_RESOURCE_TYPE: &str = "gts.cf.core.am.tenant.v1~"; +pub const TENANT_RESOURCE_TYPE: &str = gts_id!("cf.core.am.tenant.v1~"); /// AM `TenantMetadata` resource. Used for every canonical error /// raised by the metadata surface — both "schema not registered" and @@ -41,12 +35,12 @@ pub const TENANT_RESOURCE_TYPE: &str = "gts.cf.core.am.tenant.v1~"; /// `type_id` string the caller supplied so consumers can still see /// **which** schema was involved without needing a separate /// `resource_type` discriminator. -pub const TENANT_METADATA_RESOURCE_TYPE: &str = "gts.cf.core.am.tenant_metadata.v1~"; +pub const TENANT_METADATA_RESOURCE_TYPE: &str = gts_id!("cf.core.am.tenant_metadata.v1~"); /// AM `ConversionRequest` resource. Used for canonical errors raised /// by the conversion-request feature and for the future PEP gate on /// conversion read / approve / reject endpoints. -pub const CONVERSION_REQUEST_RESOURCE_TYPE: &str = "gts.cf.core.am.conversion_request.v1~"; +pub const CONVERSION_REQUEST_RESOURCE_TYPE: &str = gts_id!("cf.core.am.conversion_request.v1~"); /// AM `IdpUser` resource projection. Mirror of the /// `gts.cf.core.am.user.v1~` JSON Schema declared in @@ -54,7 +48,7 @@ pub const CONVERSION_REQUEST_RESOURCE_TYPE: &str = "gts.cf.core.am.conversion_re /// and produced by [`crate::IdpUser`]. Surfaces as the `resource_type` /// on user-scoped canonical errors raised by the user-operations /// feature (`feature-idp-user-operations-contract`). -pub const USER_RESOURCE_TYPE: &str = "gts.cf.core.am.user.v1~"; +pub const USER_RESOURCE_TYPE: &str = gts_id!("cf.core.am.user.v1~"); // --------------------------------------------------------------------------- // IdpUser-groups feature -- two flavours of identifiers @@ -80,7 +74,7 @@ pub const USER_RESOURCE_TYPE: &str = "gts.cf.core.am.user.v1~"; /// The string lives in RG's type-registry namespace /// (`gts.cf.core.rg.type.v1~` prefix) as required by RG's /// `validate_type_code`. -pub const USER_GROUP_RG_TYPE_CODE: &str = "gts.cf.core.rg.type.v1~cf.core.am.user_group.v1~"; +pub const USER_GROUP_RG_TYPE_CODE: &str = gts_id!("cf.core.rg.type.v1~cf.core.am.user_group.v1~"); /// RG type-registry code for the AM user **member-handle** type. /// @@ -96,7 +90,7 @@ pub const USER_GROUP_RG_TYPE_CODE: &str = "gts.cf.core.rg.type.v1~cf.core.am.use /// This is a type-registry-only entry; AM users themselves live in /// AM's tables + `IdP`, never as RG groups. Wraps /// [`USER_RESOURCE_TYPE`] in the RG type-registry namespace. -pub const USER_RG_TYPE_CODE: &str = "gts.cf.core.rg.type.v1~cf.core.am.user.v1~"; +pub const USER_RG_TYPE_CODE: &str = gts_id!("cf.core.rg.type.v1~cf.core.am.user.v1~"); // --------------------------------------------------------------------------- // AM base envelope schema registration @@ -133,10 +127,9 @@ pub const USER_RG_TYPE_CODE: &str = "gts.cf.core.rg.type.v1~cf.core.am.user.v1~" // AM resource type schema mirrors // --------------------------------------------------------------------------- -use gts_macros::GtsTraitsSchema; use schemars::JsonSchema; use toolkit::gts::PluginV1; -use toolkit_gts::gts_type_schema; +use toolkit_gts::{GtsTraitsSchema, gts_id, gts_type_schema}; /// GTS Type Schema mirror for the AM `IdpUser` resource type /// (`gts.cf.core.am.user.v1~`). @@ -173,7 +166,7 @@ use toolkit_gts::gts_type_schema; /// `user_schema_constraints_tests` guards against regression. #[gts_type_schema( dir_path = "schemas", - type_id = "gts.cf.core.am.user.v1~", + type_id = gts_id!("cf.core.am.user.v1~"), description = "Account Management user resource — IdP-issued user identity projection", properties = "id,username,email,display_name,first_name,last_name", base = true @@ -247,7 +240,7 @@ pub struct UserV1 { #[gts_type_schema( dir_path = "schemas", base = PluginV1, - type_id = "gts.cf.toolkit.plugins.plugin.v1~cf.core.idp.plugin.v1~", + type_id = gts_id!("cf.toolkit.plugins.plugin.v1~cf.core.idp.plugin.v1~"), description = "IdP provider plugin specification", properties = "", )] @@ -275,7 +268,7 @@ pub struct IdpPluginSpecV1; /// `tenant_schema_constraints_tests`. #[gts_type_schema( dir_path = "schemas", - type_id = "gts.cf.core.am.tenant.v1~", + type_id = gts_id!("cf.core.am.tenant.v1~"), description = "Account Management tenant resource — RBAC/PEP target type", properties = "id,name", base = true @@ -295,7 +288,7 @@ pub struct TenantV1 { /// `pep::CONVERSION`). See [`TenantV1`] for the RBAC-authorizability rationale. #[gts_type_schema( dir_path = "schemas", - type_id = "gts.cf.core.am.conversion_request.v1~", + type_id = gts_id!("cf.core.am.conversion_request.v1~"), description = "Account Management tenant conversion-request resource — RBAC/PEP target type", properties = "id", base = true @@ -322,18 +315,16 @@ pub struct ConversionRequestV1 { // A single `allowed_parent_types` entry: the GTS type identifier of a // tenant type permitted as parent. The `x-gts-ref` extension makes the // gts store validate each value is a `tenant_type.v1~`-derived type id -// when a derived tenant-type schema is registered. The value is a bare -// `gts.`-prefixed literal, which per GTS spec §9.6 is enforced as a -// `startsWith` prefix check (no glob `*` — that is non-canonical; only -// the literal `gts.*` is a wildcard). The prefix also permits same-type -// nesting (a type listing its own id); `/$id` self-reference is a -// different, exact-match construct and is not what we want here. +// when a derived tenant-type schema is registered. The prefix also +// permits same-type nesting (a type listing its own id); `/$id` +// self-reference is a different, exact-match construct and is not what +// we want here. // `transparent` so the schema is just the inner string + the extension // (no wrapper title) and the value serialises as a bare string. #[allow(dead_code)] #[derive(serde::Serialize, JsonSchema)] #[serde(transparent)] -#[schemars(transparent, extend("x-gts-ref" = "gts.cf.core.am.tenant_type.v1~"))] +#[schemars(transparent, extend("x-gts-ref" = gts_id!("cf.core.am.tenant_type.v1~")))] pub struct TenantTypeRef(pub String); #[allow(clippy::doc_markdown)] @@ -355,7 +346,7 @@ pub struct TenantTypeTraits { #[gts_type_schema( dir_path = "schemas", base = true, - type_id = "gts.cf.core.am.tenant_type.v1~", + type_id = gts_id!("cf.core.am.tenant_type.v1~"), description = "Base tenant type schema for Account Management. Derived tenant type schemas resolve behavioral traits via x-gts-traits. Traits configure system behavior for processing tenants of each type - they are not part of the tenant instance data model.", properties = "id", traits_schema = inline(TenantTypeTraits), @@ -412,7 +403,7 @@ pub struct TenantMetadataTraits { #[gts_type_schema( dir_path = "schemas", base = true, - type_id = "gts.cf.core.am.tenant_metadata.v1~", + type_id = gts_id!("cf.core.am.tenant_metadata.v1~"), description = "Base tenant metadata schema for Account Management. Derived metadata schemas resolve behavioral traits via x-gts-traits. Traits configure how MetadataService treats entries of each schema - they are not part of the metadata payload.", properties = "id", traits_schema = inline(TenantMetadataTraits), diff --git a/gears/system/account-management/account-management-sdk/src/idp.rs b/gears/system/account-management/account-management-sdk/src/idp.rs index 1069dce72..f85c9f770 100644 --- a/gears/system/account-management/account-management-sdk/src/idp.rs +++ b/gears/system/account-management/account-management-sdk/src/idp.rs @@ -135,7 +135,7 @@ pub struct IdpProvisionTenantRequest { /// is self-describing for plugin authors and surfaces with /// `format: gts-schema-id` in any generated JSON Schema. The /// wire shape stays a string; AM-side consumers run full chain - /// validation by passing the value through `gts::GtsID::new`. + /// validation by passing the value through `gts::GtsId::try_new`. pub tenant_type: GtsTypeId, /// Opaque provider-specific metadata from `TenantCreateRequest.provisioning_metadata`. pub metadata: Option, diff --git a/gears/system/account-management/account-management-sdk/src/idp_tests.rs b/gears/system/account-management/account-management-sdk/src/idp_tests.rs index d105a63c1..2a1145059 100644 --- a/gears/system/account-management/account-management-sdk/src/idp_tests.rs +++ b/gears/system/account-management/account-management-sdk/src/idp_tests.rs @@ -2,6 +2,7 @@ //! and the metric-label constants on the failure enums. use super::*; +use toolkit_gts::gts_id; use crate::{IdpNewUser, IdpTenantContext, IdpUserPagination}; use async_trait::async_trait; @@ -20,7 +21,7 @@ fn sample_tenant_context() -> IdpTenantContext { IdpTenantContext::new( Uuid::nil(), "t", - gts::GtsTypeId::new("gts.cf.core.am.tenant_type.v1~cf.core.am.customer.v1~"), + gts::GtsTypeId::new(gts_id!("cf.core.am.tenant_type.v1~cf.core.am.customer.v1~")), None, ) } @@ -49,7 +50,7 @@ async fn provision_tenant_default_impl_returns_unsupported_operation() { let req = IdpProvisionTenantRequest::for_root( Uuid::nil(), "t", - gts::GtsTypeId::new("gts.cf.core.am.tenant_type.v1~cf.core.am.customer.v1~"), + gts::GtsTypeId::new(gts_id!("cf.core.am.tenant_type.v1~cf.core.am.customer.v1~")), ); let err = s .provision_tenant(&sample_security_context(), &req) @@ -108,7 +109,7 @@ async fn list_users_default_impl_returns_unsupported_operation() { #[test] fn parent_context_defaults_none_and_builder_sets_it() { - let tt = gts::GtsTypeId::new("gts.cf.core.am.tenant_type.v1~cf.core.am.customer.v1~"); + let tt = gts::GtsTypeId::new(gts_id!("cf.core.am.tenant_type.v1~cf.core.am.customer.v1~")); let req = IdpProvisionTenantRequest::new(Uuid::nil(), Uuid::nil(), "t", tt.clone()); assert!( req.parent_context.is_none(), diff --git a/gears/system/account-management/account-management-sdk/src/idp_user_tests.rs b/gears/system/account-management/account-management-sdk/src/idp_user_tests.rs index 276604d51..9b73dca0e 100644 --- a/gears/system/account-management/account-management-sdk/src/idp_user_tests.rs +++ b/gears/system/account-management/account-management-sdk/src/idp_user_tests.rs @@ -9,11 +9,13 @@ #![allow(clippy::expect_used, clippy::unwrap_used, reason = "test helpers")] use super::*; +use toolkit_gts::gts_id; #[test] fn tenant_context_new_carries_inputs_verbatim() { let id = Uuid::from_u128(0x42); - let tenant_type = gts::GtsTypeId::new("gts.cf.core.am.tenant_type.v1~cf.core.am.customer.v1~"); + let tenant_type = + gts::GtsTypeId::new(gts_id!("cf.core.am.tenant_type.v1~cf.core.am.customer.v1~")); let ctx = IdpTenantContext::new(id, "acme", tenant_type.clone(), None); assert_eq!(ctx.tenant_id, id); assert_eq!(ctx.tenant_name, "acme"); @@ -24,7 +26,8 @@ fn tenant_context_new_carries_inputs_verbatim() { #[test] fn tenant_context_new_with_metadata_populates_field() { let id = Uuid::from_u128(0x43); - let tenant_type = gts::GtsTypeId::new("gts.cf.core.am.tenant_type.v1~cf.core.am.customer.v1~"); + let tenant_type = + gts::GtsTypeId::new(gts_id!("cf.core.am.tenant_type.v1~cf.core.am.customer.v1~")); let metadata = serde_json::json!({"realm": "acme-prod"}); let ctx = IdpTenantContext::new(id, "acme", tenant_type.clone(), Some(metadata.clone())); assert_eq!(ctx.tenant_type, tenant_type); @@ -36,7 +39,7 @@ fn tenant_context_serde_skips_absent_metadata() { // `metadata = None` is the default-and-most-common shape for // plugins that bind via external configuration; the wire payload // stays minimal in that case. - let tenant_type = gts::GtsTypeId::new("gts.cf.core.am.tenant_type.v1~cf.core.am.x.v1~"); + let tenant_type = gts::GtsTypeId::new(gts_id!("cf.core.am.tenant_type.v1~cf.core.am.x.v1~")); let ctx = IdpTenantContext::new(Uuid::from_u128(0x44), "acme", tenant_type.clone(), None); let json = serde_json::to_value(&ctx).expect("serialise"); let obj = json.as_object().expect("object"); @@ -411,7 +414,7 @@ fn idp_list_users_request_carries_typed_filter_and_order() { let ctx = IdpTenantContext::new( Uuid::from_u128(1), "acme", - gts::GtsTypeId::new("gts.cf.core.am.tenant_type.v1~cf.core.am.customer.v1~"), + gts::GtsTypeId::new(gts_id!("cf.core.am.tenant_type.v1~cf.core.am.customer.v1~")), None, ); let pagination = IdpUserPagination::default(); @@ -445,7 +448,7 @@ fn idp_list_users_request_new_defaults_filter_and_order_to_none() { let ctx = IdpTenantContext::new( Uuid::from_u128(2), "acme", - gts::GtsTypeId::new("gts.cf.core.am.tenant_type.v1~cf.core.am.customer.v1~"), + gts::GtsTypeId::new(gts_id!("cf.core.am.tenant_type.v1~cf.core.am.customer.v1~")), None, ); let req = IdpListUsersRequest::new(ctx, IdpUserPagination::default()); diff --git a/gears/system/account-management/account-management-sdk/src/metadata.rs b/gears/system/account-management/account-management-sdk/src/metadata.rs index a0cbb91e6..8efce35f2 100644 --- a/gears/system/account-management/account-management-sdk/src/metadata.rs +++ b/gears/system/account-management/account-management-sdk/src/metadata.rs @@ -177,7 +177,7 @@ pub struct MetadataEntryQuery { #[odata(filter(kind = "DateTimeUtc"))] pub updated_at: OffsetDateTime, /// Deterministic `UUIDv5` derived server-side from the chained - /// `type_id` (via the upstream `gts::GtsID::to_uuid()` namespace). + /// `type_id` (via the upstream `gts::GtsId::to_uuid()` namespace). /// Exposed as a filter / order field so the repo layer can /// stable-sort rows whose `updated_at` collide. Callers can also /// use it for an exact `$filter=schema_uuid eq ` lookup diff --git a/gears/system/account-management/account-management-sdk/src/metadata_tests.rs b/gears/system/account-management/account-management-sdk/src/metadata_tests.rs index eb95a69e9..d8c230ba8 100644 --- a/gears/system/account-management/account-management-sdk/src/metadata_tests.rs +++ b/gears/system/account-management/account-management-sdk/src/metadata_tests.rs @@ -17,9 +17,11 @@ use super::*; use serde_json::json; use time::macros::datetime; +use toolkit_gts::gts_id; /// Canonical valid chained schema id used across positive-path tests. -const VALID_TYPE_ID: &str = "gts.cf.core.am.tenant_metadata.v1~vendor.app.metadata.branding.v1~"; +const VALID_TYPE_ID: &str = + gts_id!("cf.core.am.tenant_metadata.v1~vendor.app.metadata.branding.v1~"); fn valid_type_id() -> GtsTypeId { GtsTypeId::new(VALID_TYPE_ID) diff --git a/gears/system/account-management/account-management-sdk/src/tenant.rs b/gears/system/account-management/account-management-sdk/src/tenant.rs index ceff0e041..aa62d5fbe 100644 --- a/gears/system/account-management/account-management-sdk/src/tenant.rs +++ b/gears/system/account-management/account-management-sdk/src/tenant.rs @@ -117,7 +117,7 @@ pub struct Tenant { /// /// `tenant_type` is a chained GTS identifier (e.g. /// `gts.cf.core.am.tenant_type.v1~cf.core.am.customer.v1~`); AM derives -/// the canonical `UUIDv5` via [`gts::GtsID`] internally so callers do not +/// the canonical `UUIDv5` via [`gts::GtsId`] internally so callers do not /// have to supply two parallel identifiers that can diverge. The field /// is typed [`GtsTypeId`] rather than `String` so callers (REST /// handler, inter-gear Rust consumers) get a self-documenting diff --git a/gears/system/account-management/account-management/src/api/rest/dto_tests.rs b/gears/system/account-management/account-management/src/api/rest/dto_tests.rs index 7fa16f143..62c54836b 100644 --- a/gears/system/account-management/account-management/src/api/rest/dto_tests.rs +++ b/gears/system/account-management/account-management/src/api/rest/dto_tests.rs @@ -18,6 +18,7 @@ use serde_json::{Value, json}; use time::OffsetDateTime; use time::macros::datetime; +use toolkit_gts::gts_id; use uuid::Uuid; use account_management_sdk::{IdpUser, MetadataEntry, Tenant, TenantId, TenantStatus}; @@ -36,7 +37,11 @@ fn sample_tenant() -> Uuid { } fn sample_schema() -> &'static str { - "gts.cf.core.am.tenant_metadata.v1~vendor.app.metadata.theme.v1~" + gts_id!("cf.core.am.tenant_metadata.v1~vendor.app.metadata.theme.v1~") +} + +fn sample_tenant_type() -> &'static str { + gts_id!("cf.core.am.tenant_type.v1~vendor.app.tenant.customer.v1~") } fn sample_updated() -> OffsetDateTime { @@ -395,7 +400,7 @@ fn sample_active_tenant() -> Tenant { id: sample_tenant_id(), name: "acme corp".into(), status: TenantStatus::Active, - tenant_type: Some("gts.cf.core.am.tenant_type.v1~vendor.app.customer.v1~".into()), + tenant_type: Some(sample_tenant_type().into()), parent_id: Some(sample_parent_id()), self_managed: false, depth: 2, @@ -416,7 +421,7 @@ fn tenant_dto_active_wire_shape_mirrors_openapi() { "id": "33333333-3333-3333-3333-333333333333", "name": "acme corp", "status": "active", - "tenant_type": "gts.cf.core.am.tenant_type.v1~vendor.app.customer.v1~", + "tenant_type": sample_tenant_type(), "parent_id": "44444444-4444-4444-4444-444444444444", "self_managed": false, "depth": 2, @@ -483,18 +488,16 @@ fn tenant_create_request_required_fields_only_deserialise() { // `name`, `parent_id`, `tenant_type` are the required wire fields; // `self_managed` defaults to `false`, `provisioning_metadata` to // `None`. The lowering generates a fresh UUIDv4 for the child id. - let raw = r#"{ + let raw = json!({ "name": "acme corp", "parent_id": "44444444-4444-4444-4444-444444444444", - "tenant_type": "gts.cf.core.am.tenant_type.v1~vendor.app.customer.v1~" - }"#; - let dto: TenantCreateRequestDto = serde_json::from_str(raw).unwrap(); + "tenant_type": sample_tenant_type() + }) + .to_string(); + let dto: TenantCreateRequestDto = serde_json::from_str(&raw).unwrap(); assert_eq!(dto.name, "acme corp"); assert_eq!(dto.parent_id, sample_parent_id().0); - assert_eq!( - dto.tenant_type, - "gts.cf.core.am.tenant_type.v1~vendor.app.customer.v1~" - ); + assert_eq!(dto.tenant_type, sample_tenant_type()); assert!(!dto.self_managed); assert!(dto.provisioning_metadata.is_none()); @@ -512,14 +515,15 @@ fn tenant_create_request_required_fields_only_deserialise() { #[test] fn tenant_create_request_full_payload_round_trips_into_sdk() { - let raw = r#"{ + let raw = json!({ "name": "acme child", "parent_id": "44444444-4444-4444-4444-444444444444", - "tenant_type": "gts.cf.core.am.tenant_type.v1~vendor.app.customer.v1~", + "tenant_type": sample_tenant_type(), "self_managed": true, "provisioning_metadata": {"vendor": "okta", "domain": "acme.example"} - }"#; - let dto: TenantCreateRequestDto = serde_json::from_str(raw).unwrap(); + }) + .to_string(); + let dto: TenantCreateRequestDto = serde_json::from_str(&raw).unwrap(); let request = dto.into_sdk_create_request(); assert!(request.self_managed); assert_eq!( @@ -571,13 +575,14 @@ fn tenant_create_request_rejects_non_object_provisioning_metadata() { // plugin (which has no input validation in this layer). The // happy path with a JSON object is covered by // `tenant_create_request_full_payload_round_trips_into_sdk`. - let raw_array = r#"{ + let raw_array = json!({ "name": "acme corp", "parent_id": "44444444-4444-4444-4444-444444444444", - "tenant_type": "gts.cf.core.am.tenant_type.v1~vendor.app.customer.v1~", + "tenant_type": sample_tenant_type(), "provisioning_metadata": [1, 2, 3] - }"#; - let err = serde_json::from_str::(raw_array) + }) + .to_string(); + let err = serde_json::from_str::(&raw_array) .expect_err("provisioning_metadata must be object | null per yaml"); // serde reports the typed deserialise mismatch by shape ("expected // a map") rather than by field name. Either signal is fine as long @@ -588,25 +593,27 @@ fn tenant_create_request_rejects_non_object_provisioning_metadata() { "error pinpoints the type mismatch: got `{err}`", ); - let raw_string = r#"{ + let raw_string = json!({ "name": "acme corp", "parent_id": "44444444-4444-4444-4444-444444444444", - "tenant_type": "gts.cf.core.am.tenant_type.v1~vendor.app.customer.v1~", + "tenant_type": sample_tenant_type(), "provisioning_metadata": "literal" - }"#; + }) + .to_string(); assert!( - serde_json::from_str::(raw_string).is_err(), + serde_json::from_str::(&raw_string).is_err(), "string provisioning_metadata must also be rejected", ); // `null` explicitly is admissible per yaml `type: [object, 'null']`. - let raw_null = r#"{ + let raw_null = json!({ "name": "acme corp", "parent_id": "44444444-4444-4444-4444-444444444444", - "tenant_type": "gts.cf.core.am.tenant_type.v1~vendor.app.customer.v1~", + "tenant_type": sample_tenant_type(), "provisioning_metadata": null - }"#; - let dto: TenantCreateRequestDto = serde_json::from_str(raw_null).unwrap(); + }) + .to_string(); + let dto: TenantCreateRequestDto = serde_json::from_str(&raw_null).unwrap(); assert!(dto.provisioning_metadata.is_none()); } @@ -673,13 +680,14 @@ fn tenant_create_request_rejects_unknown_fields() { // field) would otherwise receive `201 Created` for a // server-allocated UUID DIFFERENT from the one they sent — // believing it created a specific tenant id when it did not. - let raw = r#"{ + let raw = json!({ "name": "acme corp", "parent_id": "44444444-4444-4444-4444-444444444444", - "tenant_type": "gts.cf.core.am.tenant_type.v1~vendor.app.customer.v1~", + "tenant_type": sample_tenant_type(), "child_id": "55555555-5555-5555-5555-555555555555" - }"#; - let err = serde_json::from_str::(raw) + }) + .to_string(); + let err = serde_json::from_str::(&raw) .expect_err("child_id is not part of the wire contract"); assert!( err.to_string().contains("child_id") || err.to_string().contains("unknown"), diff --git a/gears/system/account-management/account-management/src/api/rest/handlers/metadata_tests.rs b/gears/system/account-management/account-management/src/api/rest/handlers/metadata_tests.rs index fe538247a..13630cd9a 100644 --- a/gears/system/account-management/account-management/src/api/rest/handlers/metadata_tests.rs +++ b/gears/system/account-management/account-management/src/api/rest/handlers/metadata_tests.rs @@ -37,6 +37,7 @@ )] use std::sync::Arc; +use toolkit_gts::gts_id; use axum::Extension; use axum::extract::Path; @@ -87,15 +88,19 @@ fn ctx() -> SecurityContext { } fn schema_a() -> GtsTypeId { - GtsTypeId::new("gts.cf.core.am.tenant_metadata.v1~vendor.app.metadata.theme.v1~") + GtsTypeId::new(gts_id!( + "cf.core.am.tenant_metadata.v1~vendor.app.metadata.theme.v1~" + )) } fn schema_b() -> GtsTypeId { - GtsTypeId::new("gts.cf.core.am.tenant_metadata.v1~vendor.app.metadata.billing.v1~") + GtsTypeId::new(gts_id!( + "cf.core.am.tenant_metadata.v1~vendor.app.metadata.billing.v1~" + )) } fn schema_a_raw() -> String { - "gts.cf.core.am.tenant_metadata.v1~vendor.app.metadata.theme.v1~".to_owned() + gts_id!("cf.core.am.tenant_metadata.v1~vendor.app.metadata.theme.v1~").to_owned() } /// Seed a single tenant + its `(subject_root, id, 0)` / @@ -142,7 +147,7 @@ async fn seed_metadata_row( type_id: &GtsTypeId, value: serde_json::Value, ) { - let schema_uuid = gts::GtsID::new(type_id.as_ref()) + let schema_uuid = gts::GtsId::try_new(type_id.as_ref()) .expect("valid GTS id in tests") .to_uuid(); let scope = toolkit_security::AccessScope::allow_all(); diff --git a/gears/system/account-management/account-management/src/api/rest/handlers/tenants_tests.rs b/gears/system/account-management/account-management/src/api/rest/handlers/tenants_tests.rs index 9c6d8970e..c0d9f7b4c 100644 --- a/gears/system/account-management/account-management/src/api/rest/handlers/tenants_tests.rs +++ b/gears/system/account-management/account-management/src/api/rest/handlers/tenants_tests.rs @@ -50,6 +50,7 @@ )] use std::sync::Arc; +use toolkit_gts::gts_id; use axum::http::{StatusCode, Uri, header}; use axum::response::IntoResponse; @@ -93,7 +94,9 @@ fn sample_active_tenant() -> Tenant { id: TenantId(sample_tenant_id()), name: "acme corp".into(), status: TenantStatus::Active, - tenant_type: Some("gts.cf.core.am.tenant_type.v1~vendor.app.customer.v1~".into()), + tenant_type: Some( + gts_id!("cf.core.am.tenant_type.v1~vendor.app.tenant.customer.v1~").into(), + ), parent_id: Some(TenantId(sample_parent_id())), self_managed: false, depth: 2, diff --git a/gears/system/account-management/account-management/src/domain/bootstrap/config_tests.rs b/gears/system/account-management/account-management/src/domain/bootstrap/config_tests.rs index 1ccf541ea..eaa7c4f66 100644 --- a/gears/system/account-management/account-management/src/domain/bootstrap/config_tests.rs +++ b/gears/system/account-management/account-management/src/domain/bootstrap/config_tests.rs @@ -8,6 +8,7 @@ )] use std::time::Duration; +use toolkit_gts::gts_id; use super::*; @@ -47,9 +48,9 @@ fn validate_accepts_fully_specified_config() { let cfg = BootstrapConfig { root_id: Uuid::from_u128(0xAA), root_name: "platform-root".into(), - root_tenant_type: gts::GtsTypeId::new( - "gts.cf.core.am.tenant_type.v1~cf.core.am.platform.v1~", - ), + root_tenant_type: gts::GtsTypeId::new(gts_id!( + "cf.core.am.tenant_type.v1~cf.core.am.platform.v1~" + )), root_tenant_metadata: None, idp_wait_timeout: Duration::from_mins(5), idp_retry_backoff_initial: Duration::from_secs(2), @@ -64,9 +65,9 @@ fn validate_rejects_zero_idp_wait_timeout() { let cfg = BootstrapConfig { root_id: Uuid::from_u128(0xAA), root_name: "platform-root".into(), - root_tenant_type: gts::GtsTypeId::new( - "gts.cf.core.am.tenant_type.v1~cf.core.am.platform.v1~", - ), + root_tenant_type: gts::GtsTypeId::new(gts_id!( + "cf.core.am.tenant_type.v1~cf.core.am.platform.v1~" + )), root_tenant_metadata: None, idp_wait_timeout: Duration::ZERO, idp_retry_backoff_initial: Duration::from_secs(2), @@ -85,9 +86,9 @@ fn validate_rejects_idp_wait_timeout_above_cap() { let cfg = BootstrapConfig { root_id: Uuid::from_u128(0xAA), root_name: "platform-root".into(), - root_tenant_type: gts::GtsTypeId::new( - "gts.cf.core.am.tenant_type.v1~cf.core.am.platform.v1~", - ), + root_tenant_type: gts::GtsTypeId::new(gts_id!( + "cf.core.am.tenant_type.v1~cf.core.am.platform.v1~" + )), root_tenant_metadata: None, // One past the documented cap; with no upper bound the // deadline math `Instant::now() + idp_wait_timeout` and the @@ -109,9 +110,9 @@ fn validate_accepts_idp_wait_timeout_at_cap() { let cfg = BootstrapConfig { root_id: Uuid::from_u128(0xAA), root_name: "platform-root".into(), - root_tenant_type: gts::GtsTypeId::new( - "gts.cf.core.am.tenant_type.v1~cf.core.am.platform.v1~", - ), + root_tenant_type: gts::GtsTypeId::new(gts_id!( + "cf.core.am.tenant_type.v1~cf.core.am.platform.v1~" + )), root_tenant_metadata: None, idp_wait_timeout: MAX_IDP_WAIT_TIMEOUT, idp_retry_backoff_initial: Duration::from_secs(2), @@ -126,9 +127,9 @@ fn validate_rejects_inverted_backoff_envelope() { let cfg = BootstrapConfig { root_id: Uuid::from_u128(0xAA), root_name: "platform-root".into(), - root_tenant_type: gts::GtsTypeId::new( - "gts.cf.core.am.tenant_type.v1~cf.core.am.platform.v1~", - ), + root_tenant_type: gts::GtsTypeId::new(gts_id!( + "cf.core.am.tenant_type.v1~cf.core.am.platform.v1~" + )), root_tenant_metadata: None, idp_wait_timeout: Duration::from_mins(5), idp_retry_backoff_initial: Duration::from_mins(1), diff --git a/gears/system/account-management/account-management/src/domain/bootstrap/service.rs b/gears/system/account-management/account-management/src/domain/bootstrap/service.rs index e54ac20d5..f97801b50 100644 --- a/gears/system/account-management/account-management/src/domain/bootstrap/service.rs +++ b/gears/system/account-management/account-management/src/domain/bootstrap/service.rs @@ -35,6 +35,7 @@ use std::sync::Arc; use std::time::Duration; +use toolkit_gts::gts_id; use time::OffsetDateTime; use tokio::time::Instant; @@ -895,9 +896,14 @@ impl BootstrapService { // `GtsTypeSchema` is by construction a type-schema (the // `is_schema` axis is implicit in the type), so the only check - // remaining is the AM-tenant-type chain prefix on `type_id`. + // remaining is that the chain root is AM's tenant_type root. let entity_type_id = entity.type_id.as_ref(); - if !entity_type_id.starts_with("gts.cf.core.am.tenant_type.v1~") { + let is_am_tenant_type = gts::GtsId::try_new(entity_type_id) + .ok() + .filter(gts::GtsId::is_type) + .and_then(|id| id.chain_ids().into_iter().next()) + .is_some_and(|root| root == gts_id!("cf.core.am.tenant_type.v1~")); + if !is_am_tenant_type { emit_metric( AM_BOOTSTRAP_LIFECYCLE, MetricKind::Counter, @@ -1056,14 +1062,14 @@ impl BootstrapService { // (`parent_id IS NULL AND depth = 0`) and the // `ux_tenants_single_root` partial unique index. // Derive `tenant_type_uuid` from the configured GTS id via - // `gts::GtsID::to_uuid()` — the same canonical V5-UUID + // `gts::GtsId::to_uuid()` — the same canonical V5-UUID // algorithm `create_tenant` uses, so the bootstrap and // child-create paths produce identical FK values for the - // same `root_tenant_type`. `GtsID::new` additionally + // same `root_tenant_type`. `GtsId::try_new` additionally // validates the chain shape, surfacing // `DomainError::InvalidTenantType` early on a malformed // configuration rather than at the FK insert. - let tenant_type_uuid = gts::GtsID::new(self.cfg.root_tenant_type.as_ref()) + let tenant_type_uuid = gts::GtsId::try_new(self.cfg.root_tenant_type.as_ref()) .map_err(|e| DomainError::InvalidTenantType { detail: format!( "invalid root_tenant_type chain `{}`: {e}", diff --git a/gears/system/account-management/account-management/src/domain/bootstrap/service_tests.rs b/gears/system/account-management/account-management/src/domain/bootstrap/service_tests.rs index fb05da093..d3a8b0d2a 100644 --- a/gears/system/account-management/account-management/src/domain/bootstrap/service_tests.rs +++ b/gears/system/account-management/account-management/src/domain/bootstrap/service_tests.rs @@ -31,6 +31,7 @@ use std::collections::HashMap; use std::sync::Arc; use time::OffsetDateTime; use toolkit_canonical_errors::CanonicalError; +use toolkit_gts::gts_id; use types_registry_sdk::{ GtsInstance, GtsTypeId, GtsTypeSchema, InstanceQuery, RegisterResult, TypeSchemaQuery, TypesRegistryClient, @@ -38,8 +39,10 @@ use types_registry_sdk::{ use uuid::Uuid; const ROOT_ID_RAW: u128 = 0x100; +const TENANT_TYPE_SCHEMA: &str = gts_id!("cf.core.am.tenant_type.v1~"); +const TENANT_SCHEMA: &str = gts_id!("cf.core.am.tenant.v1~"); const TENANT_TYPE_UUID_RAW: u128 = 0xAA; -const ROOT_TENANT_TYPE: &str = "gts.cf.core.am.tenant_type.v1~cf.core.am.platform.v1~"; +const ROOT_TENANT_TYPE: &str = gts_id!("cf.core.am.tenant_type.v1~cf.core.am.platform.v1~"); fn root_id() -> Uuid { Uuid::from_u128(ROOT_ID_RAW) @@ -111,11 +114,11 @@ impl StubTypesRegistry { // segment) so `GtsTypeSchema::try_new` does not require a // pre-resolved parent. Bootstrap preflight only inspects the // chain prefix on `type_id` (must start with - // `gts.cf.core.am.tenant_type.v1~`) and the optional + // the AM tenant-type root schema) and the optional // `x-gts-traits.allowed_parent_types` array (left unset so // the eligibility check passes). GtsTypeSchema::try_new( - GtsTypeId::new("gts.cf.core.am.tenant_type.v1~"), + GtsTypeId::new(TENANT_TYPE_SCHEMA), serde_json::json!({}), None, None, @@ -123,7 +126,7 @@ impl StubTypesRegistry { .expect("canned root schema must construct") } - /// Canned `gts.cf.core.am.tenant.v1~` projection schema. Pins the + /// Canned AM tenant projection schema. Pins the /// `name` field bounds (`minLength: 1, maxLength: 255`) so /// [`crate::domain::gts_validation::validate_tenant_name_via_gts`] /// — called from `insert_root_provisioning` — has a registered @@ -132,7 +135,7 @@ impl StubTypesRegistry { /// bounds would not gate the saga in tests. fn canned_tenant_schema() -> GtsTypeSchema { GtsTypeSchema::try_new( - GtsTypeId::new("gts.cf.core.am.tenant.v1~"), + GtsTypeId::new(TENANT_SCHEMA), serde_json::json!({ "type": "object", "required": ["id", "name"], @@ -167,16 +170,16 @@ impl TypesRegistryClient for StubTypesRegistry { // Dispatch by the two ids bootstrap consults: // * `ROOT_TENANT_TYPE` — preflight tenant-type eligibility // (`preflight_root_tenant_type`). - // * `gts.cf.core.am.tenant.v1~` — `root_name` structural + // * `TENANT_SCHEMA` — `root_name` structural // validation in `insert_root_provisioning` mirroring the // `create_tenant` site. Any other id is a wiring regression // and trips a loud panic, same posture as the previous // single-id assertion. match type_id { ROOT_TENANT_TYPE => Ok(Self::canned_schema()), - "gts.cf.core.am.tenant.v1~" => Ok(Self::canned_tenant_schema()), + TENANT_SCHEMA => Ok(Self::canned_tenant_schema()), other => panic!( - "bootstrap queried unexpected type_id `{other}` (expected `{ROOT_TENANT_TYPE}` or `gts.cf.core.am.tenant.v1~`)" + "bootstrap queried unexpected type_id `{other}` (expected `{ROOT_TENANT_TYPE}` or `{TENANT_SCHEMA}`)" ), } } @@ -719,7 +722,7 @@ async fn run_rejects_root_name_violating_tenant_v1_schema_via_gts() { match &err { DomainError::Validation { detail } => { assert!( - detail.contains("name") && detail.contains("gts.cf.core.am.tenant.v1~"), + detail.contains("name") && detail.contains(TENANT_SCHEMA), "Validation must name the offending field and schema, got: {detail}" ); } diff --git a/gears/system/account-management/account-management/src/domain/conversion/service.rs b/gears/system/account-management/account-management/src/domain/conversion/service.rs index 601fb4054..5e31e4f47 100644 --- a/gears/system/account-management/account-management/src/domain/conversion/service.rs +++ b/gears/system/account-management/account-management/src/domain/conversion/service.rs @@ -335,7 +335,7 @@ pub(crate) mod pep { /// compiled subtree clamp on `tenants` resolves through this /// property). pub const CONVERSION: ResourceType = ResourceType::from_static( - "gts.cf.core.am.conversion_request.v1~", + account_management_sdk::CONVERSION_REQUEST_RESOURCE_TYPE, &[pep_properties::OWNER_TENANT_ID, pep_properties::RESOURCE_ID], ); diff --git a/gears/system/account-management/account-management/src/domain/error_tests.rs b/gears/system/account-management/account-management/src/domain/error_tests.rs index 348186696..a77a7c91e 100644 --- a/gears/system/account-management/account-management/src/domain/error_tests.rs +++ b/gears/system/account-management/account-management/src/domain/error_tests.rs @@ -10,6 +10,7 @@ //! envelope (category, status, resource type, context tokens). use std::time::Duration; +use toolkit_gts::gts_id; use account_management_sdk::error::AccountManagementError; use toolkit_canonical_errors::CanonicalError; @@ -224,7 +225,7 @@ fn metadata_entry_not_found_carries_chained_type_id() { // the metadata resource type. let ame = project(DomainError::MetadataEntryNotFound { detail: "schema billing.v1 missing".into(), - entry: "gts.cf.core.am.tenant_metadata.v1~cf.core.billing.usage.v1~".into(), + entry: gts_id!("cf.core.am.tenant_metadata.v1~cf.core.billing.usage.v1~").into(), }); let AccountManagementError::NotFound { resource_type, @@ -236,7 +237,7 @@ fn metadata_entry_not_found_carries_chained_type_id() { }; assert_eq!( name, - "gts.cf.core.am.tenant_metadata.v1~cf.core.billing.usage.v1~" + gts_id!("cf.core.am.tenant_metadata.v1~cf.core.billing.usage.v1~") ); assert_eq!( resource_type, diff --git a/gears/system/account-management/account-management/src/domain/gts_validation.rs b/gears/system/account-management/account-management/src/domain/gts_validation.rs index e7f9978e2..4d3509afb 100644 --- a/gears/system/account-management/account-management/src/domain/gts_validation.rs +++ b/gears/system/account-management/account-management/src/domain/gts_validation.rs @@ -80,17 +80,18 @@ use account_management_sdk::IdpNewUser; use serde_json::Value; use toolkit_canonical_errors::CanonicalError; +use toolkit_gts::gts_id; use types_registry_sdk::TypesRegistryClient; use crate::domain::error::DomainError; /// GTS type id of the Account Management user resource. Pinned to the /// same string the published JSON Schema declares as its `$id`. -pub(crate) const USER_TYPE_ID: &str = "gts.cf.core.am.user.v1~"; +pub(crate) const USER_TYPE_ID: &str = gts_id!("cf.core.am.user.v1~"); /// GTS type id of the Account Management tenant resource. Pinned to /// the same string the published JSON Schema declares as its `$id`. -pub(crate) const TENANT_TYPE_ID: &str = "gts.cf.core.am.tenant.v1~"; +pub(crate) const TENANT_TYPE_ID: &str = gts_id!("cf.core.am.tenant.v1~"); /// Validate the structural fields of a [`IdpNewUser`] against the /// registered `gts.cf.core.am.user.v1~` schema. diff --git a/gears/system/account-management/account-management/src/domain/metadata/registry.rs b/gears/system/account-management/account-management/src/domain/metadata/registry.rs index f0d811e92..e48d42c5c 100644 --- a/gears/system/account-management/account-management/src/domain/metadata/registry.rs +++ b/gears/system/account-management/account-management/src/domain/metadata/registry.rs @@ -29,7 +29,7 @@ //! pairs so per-schema policy can be scripted from service-level //! unit tests. Reverse lookup uses the same map keyed by the //! deterministic `UUIDv5` derivation via upstream -//! [`gts::GtsID::to_uuid`]. +//! [`gts::GtsId::to_uuid`]. //! * `GtsMetadataSchemaRegistry` — the production implementation //! backed by `types_registry_sdk::TypesRegistryClient`. //! @@ -62,7 +62,7 @@ use std::collections::{HashMap, HashSet}; use std::sync::Arc; use async_trait::async_trait; -use gts::{GtsID, GtsTypeId}; +use gts::{GtsId, GtsTypeId}; use parking_lot::Mutex; use serde_json::Value; use toolkit_macros::domain_model; @@ -212,7 +212,7 @@ pub trait MetadataSchemaRegistry: Send + Sync { /// Compute the deterministic `schema_uuid` for an already-validated /// `type_id` string. AM-internal helper shared between Stub and -/// production registries — both rely on `gts::GtsID::to_uuid()` for +/// production registries — both rely on `gts::GtsId::to_uuid()` for /// the canonical namespace. /// /// # Panics @@ -227,7 +227,7 @@ pub trait MetadataSchemaRegistry: Send + Sync { an unparseable input here is a service-layer contract break" )] fn uuid_for_registered_schema(type_id: &GtsTypeId) -> Uuid { - GtsID::new(type_id.as_ref()) + GtsId::try_new(type_id.as_ref()) .expect( "registry was given a type_id that does not parse as a GTS id - \ caller (service layer) is contract-broken", diff --git a/gears/system/account-management/account-management/src/domain/metadata/service.rs b/gears/system/account-management/account-management/src/domain/metadata/service.rs index 52385f6af..3e38413c2 100644 --- a/gears/system/account-management/account-management/src/domain/metadata/service.rs +++ b/gears/system/account-management/account-management/src/domain/metadata/service.rs @@ -97,7 +97,7 @@ pub(crate) mod pep { /// with the row's resolved `type_id` to drop entries the /// caller cannot read — the PRD line 1848 contract. pub const METADATA: ResourceType = ResourceType::from_static( - "gts.cf.core.am.tenant_metadata.v1~", + account_management_sdk::TENANT_METADATA_RESOURCE_TYPE, &[ pep_properties::OWNER_TENANT_ID, pep_properties::RESOURCE_ID, @@ -509,7 +509,7 @@ impl MetadataService { .await?; // UUIDv5 derivation cached on `ParsedTypeId` (matches the - // upstream `gts::GtsID::to_uuid()` namespace per + // upstream `gts::GtsId::to_uuid()` namespace per // `dod-tenant-metadata-schema-registration-and-uuid-derivation`). let schema_uuid = parsed.uuid(); diff --git a/gears/system/account-management/account-management/src/domain/metadata/service_tests.rs b/gears/system/account-management/account-management/src/domain/metadata/service_tests.rs index c77430838..ea9765075 100644 --- a/gears/system/account-management/account-management/src/domain/metadata/service_tests.rs +++ b/gears/system/account-management/account-management/src/domain/metadata/service_tests.rs @@ -33,6 +33,7 @@ )] use std::sync::Arc; +use toolkit_gts::gts_id; use account_management_sdk::{MetadataEntry, UpsertMetadataRequest}; use serde_json::{Value, json}; @@ -96,15 +97,21 @@ fn first_page() -> ODataQuery { } fn schema_a() -> GtsTypeId { - GtsTypeId::new("gts.cf.core.am.tenant_metadata.v1~vendor.app.metadata.theme.v1~") + GtsTypeId::new(gts_id!( + "cf.core.am.tenant_metadata.v1~vendor.app.metadata.theme.v1~" + )) } fn schema_b() -> GtsTypeId { - GtsTypeId::new("gts.cf.core.am.tenant_metadata.v1~vendor.app.metadata.billing.v1~") + GtsTypeId::new(gts_id!( + "cf.core.am.tenant_metadata.v1~vendor.app.metadata.billing.v1~" + )) } fn schema_unknown() -> GtsTypeId { - GtsTypeId::new("gts.cf.core.am.tenant_metadata.v1~vendor.app.metadata.absent.v1~") + GtsTypeId::new(gts_id!( + "cf.core.am.tenant_metadata.v1~vendor.app.metadata.absent.v1~" + )) } /// Compute the same deterministic `UUIDv5` the service / repo use @@ -116,7 +123,7 @@ fn schema_unknown() -> GtsTypeId { reason = "test helpers only see hand-crafted valid schema ids" )] fn schema_uuid_for(type_id: &str) -> Uuid { - gts::GtsID::new(type_id) + gts::GtsId::try_new(type_id) .expect("valid GTS id in tests") .to_uuid() } diff --git a/gears/system/account-management/account-management/src/domain/metadata/type_id.rs b/gears/system/account-management/account-management/src/domain/metadata/type_id.rs index 60d38f7e6..26d22e9a5 100644 --- a/gears/system/account-management/account-management/src/domain/metadata/type_id.rs +++ b/gears/system/account-management/account-management/src/domain/metadata/type_id.rs @@ -4,18 +4,18 @@ //! wire `type_id` string into a typed value before any registry or //! repo call. Four checks run in order: //! -//! 1. Parse the wire `type_id` string via [`gts::GtsID::new`] — +//! 1. Parse the wire `type_id` string via [`gts::GtsId::try_new`] — //! rejects malformed GTS syntax. //! 2. Require the root segment match //! [`METADATA_ROOT_SEGMENT`] (`cf.core.am.tenant_metadata.v1`) — //! rejects schemas from other namespaces. //! 3. Require at least one chained user-registered schema segment //! after the root. -//! 4. Require schema-shape (`GtsID::is_type` — every segment ends +//! 4. Require schema-shape (`GtsId::is_type` — every segment ends //! with `~`); reject instance-id shapes. //! //! On success [`ParsedTypeId`] also caches the deterministic -//! `UUIDv5` derived through [`gts::GtsID::to_uuid`] — same namespace +//! `UUIDv5` derived through [`gts::GtsId::to_uuid`] — same namespace //! the upstream `gts` crate uses internally, so AM and any sibling //! consuming the `gts` crate directly agree on the storage-side //! `schema_uuid` mapping. @@ -26,7 +26,8 @@ //! boundary. The SDK ships raw `String` for `type_id` and never //! sees the granular validation error variants. -use gts::{GtsID, GtsTypeId}; +use gts::{GtsId, GtsTypeId}; +use toolkit_gts::GTS_ID_PREFIX; use toolkit_macros::domain_model; use uuid::Uuid; @@ -60,16 +61,16 @@ impl ParsedTypeId { /// specific failure mode (malformed GTS, wrong root segment, /// missing chained segment, instance-id shape). pub(crate) fn parse(s: &str) -> Result { - let parsed = GtsID::new(s).map_err(|err| DomainError::MetadataValidation { + let parsed = GtsId::try_new(s).map_err(|err| DomainError::MetadataValidation { detail: format!("malformed metadata schema id: {err}"), })?; - let segments = &parsed.gts_id_segments; + let segments = parsed.segments(); if segments.len() < 2 { return Err(DomainError::MetadataValidation { detail: format!( "metadata schema id `{}` is missing a chained user-registered segment \ - after the root (`gts.{METADATA_ROOT_SEGMENT}`)", + after the root (`{GTS_ID_PREFIX}{METADATA_ROOT_SEGMENT}`)", parsed.as_ref() ), }); @@ -77,19 +78,19 @@ impl ParsedTypeId { // `GtsIdSegment.segment` includes the trailing `~`; strip // before comparing against the root constant. - let root_str = segments[0].segment.trim_end_matches('~'); + let root_str = segments[0].raw().trim_end_matches('~'); if root_str != METADATA_ROOT_SEGMENT { return Err(DomainError::MetadataValidation { detail: format!( - "metadata schema id must start with `gts.{METADATA_ROOT_SEGMENT}`, \ - got `gts.{root_str}`" + "metadata schema id must start with `{GTS_ID_PREFIX}{METADATA_ROOT_SEGMENT}`, \ + got `{GTS_ID_PREFIX}{root_str}`" ), }); } // Schema-shape: every segment of a schema chain ends with `~`. // An instance id whose tail segment lacks `~` parses cleanly - // as a `GtsID` but is NOT a schema chain — reject at the + // as a `GtsId` but is NOT a schema chain — reject at the // boundary so the downstream `schema_uuid` lookup does not // surface as a confusing 404. if !parsed.is_type() { @@ -101,13 +102,13 @@ impl ParsedTypeId { }); } - // `gts::GtsID::to_uuid()` hashes `self.id.as_bytes()` under the + // `gts::GtsId::to_uuid()` hashes `self.id.as_bytes()` under the // upstream `GTS_NS` (= `Uuid::new_v5(&NAMESPACE_URL, b"gts")`). // Single source of truth shared with every sibling that // imports the `gts` crate. let uuid = parsed.to_uuid(); - // Use parsed.as_ref(), not the original `s`: GtsID::new trims + // Use parsed.as_ref(), not the original `s`: GtsId::try_new trims // whitespace; storing the trimmed form keeps schema_uuid // consistent with reverse-hydration. Ok(Self { diff --git a/gears/system/account-management/account-management/src/domain/metadata/type_id_tests.rs b/gears/system/account-management/account-management/src/domain/metadata/type_id_tests.rs index 1c6d012fd..86722c3c5 100644 --- a/gears/system/account-management/account-management/src/domain/metadata/type_id_tests.rs +++ b/gears/system/account-management/account-management/src/domain/metadata/type_id_tests.rs @@ -10,19 +10,21 @@ #![allow(clippy::expect_used, clippy::unwrap_used, reason = "test helpers")] +use toolkit_gts::{GTS_ID_PREFIX, gts_id}; use uuid::Uuid; -use super::ParsedTypeId; +use super::{METADATA_ROOT_SEGMENT, ParsedTypeId}; use crate::domain::error::DomainError; /// Canonical valid chained schema id used across positive-path tests. -const VALID_TYPE_ID: &str = "gts.cf.core.am.tenant_metadata.v1~vendor.app.metadata.branding.v1~"; +const VALID_TYPE_ID: &str = + gts_id!("cf.core.am.tenant_metadata.v1~vendor.app.metadata.branding.v1~"); /// `UUIDv5` expected for [`VALID_TYPE_ID`] under the shared GTS /// namespace (`Uuid::new_v5(&Uuid::NAMESPACE_URL, b"gts")`). Hardcoded /// to pin the namespace + algorithm choice: any drift in either /// makes this test fail immediately. Computed once via -/// `gts::GtsID::new(VALID_TYPE_ID).unwrap().to_uuid()`. +/// `gts::GtsId::try_new(VALID_TYPE_ID).unwrap().to_uuid()`. const VALID_SCHEMA_UUID: &str = "1908c97f-00d4-5e43-9c33-d3904e7bcfa6"; #[test] @@ -59,14 +61,16 @@ fn parse_rejects_malformed_gts_syntax() { #[test] fn parse_rejects_wrong_root_segment() { // Valid GTS shape (5 tokens per segment), wrong AM-namespace root. - let alien = "gts.cf.core.other_gear.dataset.v1~vendor.app.foo.bar.v1~"; + let alien = gts_id!("cf.core.other_gear.dataset.v1~vendor.app.foo.bar.v1~"); let err = ParsedTypeId::parse(alien).expect_err("wrong root"); let detail = match err { DomainError::MetadataValidation { detail } => detail, other => panic!("expected MetadataValidation, got {other:?}"), }; assert!( - detail.contains("must start with `gts.cf.core.am.tenant_metadata.v1`"), + detail.contains(&format!( + "must start with `{GTS_ID_PREFIX}{METADATA_ROOT_SEGMENT}`" + )), "diagnostic should name expected root, got: {detail}" ); } @@ -74,7 +78,7 @@ fn parse_rejects_wrong_root_segment() { #[test] fn parse_rejects_root_only_chain() { // Valid root, no chained segment after it. - let root_only = "gts.cf.core.am.tenant_metadata.v1~"; + let root_only = gts_id!("cf.core.am.tenant_metadata.v1~"); let err = ParsedTypeId::parse(root_only).expect_err("root only"); let detail = match err { DomainError::MetadataValidation { detail } => detail, @@ -89,7 +93,7 @@ fn parse_rejects_root_only_chain() { #[test] fn parse_rejects_instance_id_shape() { // Tail segment lacks `~` — instance id, not a schema chain. - let instance = "gts.cf.core.am.tenant_metadata.v1~vendor.app.metadata.branding.v1"; + let instance = gts_id!("cf.core.am.tenant_metadata.v1~vendor.app.metadata.branding.v1"); let err = ParsedTypeId::parse(instance).expect_err("instance shape"); let detail = match err { DomainError::MetadataValidation { detail } => detail, @@ -104,17 +108,17 @@ fn parse_rejects_instance_id_shape() { #[test] fn uuid_matches_upstream_gts_to_uuid() { // Pin the equivalence-class contract: `ParsedTypeId::uuid()` - // MUST return the same UUID as the upstream `gts::GtsID::to_uuid()` + // MUST return the same UUID as the upstream `gts::GtsId::to_uuid()` // — that is the documented "shared namespace" guarantee. A drift // here means storage UUIDs derived in AM no longer match what // any sibling using `gts` directly would compute. let parsed = ParsedTypeId::parse(VALID_TYPE_ID).expect("valid"); - let upstream = gts::GtsID::new(VALID_TYPE_ID) + let upstream = gts::GtsId::try_new(VALID_TYPE_ID) .expect("upstream parse") .to_uuid(); assert_eq!( parsed.uuid(), upstream, - "AM-side ParsedTypeId UUID drifted from upstream gts::GtsID::to_uuid()" + "AM-side ParsedTypeId UUID drifted from upstream gts::GtsId::to_uuid()" ); } diff --git a/gears/system/account-management/account-management/src/domain/tenant/service/mod.rs b/gears/system/account-management/account-management/src/domain/tenant/service/mod.rs index 02732aa17..8d107b375 100644 --- a/gears/system/account-management/account-management/src/domain/tenant/service/mod.rs +++ b/gears/system/account-management/account-management/src/domain/tenant/service/mod.rs @@ -27,6 +27,7 @@ use uuid::Uuid; /// match DESIGN §4.2 line 1363; renaming them is a contract change. pub(crate) mod pep { use super::{ResourceType, pep_properties}; + use account_management_sdk::TENANT_RESOURCE_TYPE; /// `Tenant` resource — `gts.cf.core.am.tenant.v1~`. /// @@ -55,7 +56,7 @@ pub(crate) mod pep { /// absent for `create` / `list_children` which have no single /// target tenant). pub const TENANT: ResourceType = ResourceType::from_static( - "gts.cf.core.am.tenant.v1~", + TENANT_RESOURCE_TYPE, &[pep_properties::OWNER_TENANT_ID, pep_properties::RESOURCE_ID], ); @@ -656,7 +657,7 @@ impl TenantService { .await?; // Pure parse — derives the canonical UUIDv5 from the chained - // GTS string. No IO. `gts::GtsID::new` enforces the chain + // GTS string. No IO. `gts::GtsId::try_new` enforces the chain // shape; `to_uuid()` is the same algorithm Types Registry // uses internally (`types-registry-sdk/src/models.rs:152`), // so the derived uuid is the lookup key the registry stores @@ -664,7 +665,7 @@ impl TenantService { // malformed `tenant_type` string fails fast as // `InvalidTenantType` rather than after a wasted Types // Registry round-trip. - let tenant_type_uuid = gts::GtsID::new(input.tenant_type.as_ref()) + let tenant_type_uuid = gts::GtsId::try_new(input.tenant_type.as_ref()) .map_err(|e| DomainError::InvalidTenantType { detail: format!("invalid tenant_type chain `{}`: {e}", input.tenant_type), })? diff --git a/gears/system/account-management/account-management/src/domain/tenant/service/service_tests.rs b/gears/system/account-management/account-management/src/domain/tenant/service/service_tests.rs index 83b732b6d..a1d93070c 100644 --- a/gears/system/account-management/account-management/src/domain/tenant/service/service_tests.rs +++ b/gears/system/account-management/account-management/src/domain/tenant/service/service_tests.rs @@ -3,6 +3,7 @@ //! no filesystem. use account_management_sdk::TenantStatus as PublicTenantStatus; +use toolkit_gts::gts_id; use toolkit_odata::ODataQuery; use toolkit_odata::ast::{CompareOperator, Expr, Value as OdataValue}; @@ -41,6 +42,8 @@ use std::sync::Mutex; use time::OffsetDateTime; use toolkit_security::AccessScope; +const CUSTOMER_TENANT_TYPE_ID: &str = gts_id!("cf.core.am.tenant_type.v1~cf.core.am.customer.v1~"); + /// Test-only `TypesRegistryClient` that resolves every UUID to the /// same hard-coded chained `GtsTypeSchema`. The reaper / retention /// pipelines (and `UserService::resolve_active_tenant`) call @@ -103,9 +106,7 @@ impl types_registry_sdk::TypesRegistryClient for ConstantTypesRegistry { &self, _type_uuid: Uuid, ) -> Result { - Ok(synth_type_schema( - "gts.cf.core.am.tenant_type.v1~cf.core.am.customer.v1~", - )) + Ok(synth_type_schema(CUSTOMER_TENANT_TYPE_ID)) } async fn get_type_schemas( &self, @@ -321,7 +322,7 @@ fn child_input(child_id: Uuid, parent_id: Uuid) -> CreateTenantRequest { child_id, parent_id, "child", - gts::GtsTypeId::new("gts.cf.core.am.tenant_type.v1~cf.core.am.customer.v1~"), + gts::GtsTypeId::new(CUSTOMER_TENANT_TYPE_ID), ) } @@ -2728,7 +2729,7 @@ async fn create_tenant_succeeds_when_parent_child_compatible() { // Root tenant_type_uuid is `0xAA` per `FakeTenantRepo::with_root`, // and the child uuid is derived from the chained-id string in - // `child_input` via `gts::GtsID::new(...).to_uuid()`. + // `child_input` via `gts::GtsId::try_new(...).to_uuid()`. let created = svc .create_tenant(&ctx_for(root), child_input(child, root)) .await @@ -2736,10 +2737,9 @@ async fn create_tenant_succeeds_when_parent_child_compatible() { assert_eq!(created.id.0, child); assert_eq!(created.status, PublicTenantStatus::Active); - let expected_child_type_uuid = - gts::GtsID::new("gts.cf.core.am.tenant_type.v1~cf.core.am.customer.v1~") - .expect("valid gts chain") - .to_uuid(); + let expected_child_type_uuid = gts::GtsId::try_new(CUSTOMER_TENANT_TYPE_ID) + .expect("valid gts chain") + .to_uuid(); let calls = checker.calls(); assert_eq!(calls.len(), 1, "barrier must be invoked exactly once"); assert_eq!(calls[0].0, Uuid::from_u128(0xAA), "parent type"); @@ -2795,7 +2795,7 @@ async fn create_tenant_rejects_same_type_nesting_when_disallowed() { // pairing). Without this alignment the test would exercise the // mixed-type path instead — see assertions at `*_calls_checker_*` // for the chain UUID derivation. - let same_type_uuid = gts::GtsID::new("gts.cf.core.am.tenant_type.v1~cf.core.am.customer.v1~") + let same_type_uuid = gts::GtsId::try_new(CUSTOMER_TENANT_TYPE_ID) .expect("valid gts chain") .to_uuid(); repo.state @@ -2847,7 +2847,7 @@ async fn create_tenant_accepts_same_type_nesting_when_allowed() { let repo = Arc::new(FakeTenantRepo::with_root(root)); // Align root tenant_type_uuid with the chain-derived child UUID — // see sibling test for the rationale. - let same_type_uuid = gts::GtsID::new("gts.cf.core.am.tenant_type.v1~cf.core.am.customer.v1~") + let same_type_uuid = gts::GtsId::try_new(CUSTOMER_TENANT_TYPE_ID) .expect("valid gts chain") .to_uuid(); repo.state diff --git a/gears/system/account-management/account-management/src/domain/user/service.rs b/gears/system/account-management/account-management/src/domain/user/service.rs index c8dc44579..0897e1fd1 100644 --- a/gears/system/account-management/account-management/src/domain/user/service.rs +++ b/gears/system/account-management/account-management/src/domain/user/service.rs @@ -80,7 +80,7 @@ const MAX_USERNAME_CHARS: usize = 255; /// `display_name`) enforced at the AM boundary as a cheap /// **pre-flight cap** before the JSON-Schema validator runs. /// -/// A missing `gts.cf.core.am.user.v1~` schema is NOT a fallback +/// A missing `USER_RESOURCE_TYPE` schema is NOT a fallback /// case for this constant — that path is fail-closed in /// [`crate::domain::gts_validation::validate_new_user_payload_via_gts`] /// (surfaces `ServiceUnavailable` until the catalog is seeded). @@ -101,6 +101,7 @@ const MAX_PROFILE_FIELD_CHARS: usize = 255; /// a cross-check test pins them in sync. pub(crate) mod pep { use super::{ResourceType, pep_properties}; + use account_management_sdk::USER_RESOURCE_TYPE; /// Resource declaration for `IdpUser`. AM persists no user table /// (per `cpt-cf-account-management-constraint-no-user-storage`), @@ -120,7 +121,7 @@ pub(crate) mod pep { /// compiled subtree clamp on `tenants` resolves through this /// property. pub const USER: ResourceType = ResourceType::from_static( - "gts.cf.core.am.user.v1~", + USER_RESOURCE_TYPE, &[pep_properties::OWNER_TENANT_ID, pep_properties::RESOURCE_ID], ); diff --git a/gears/system/account-management/account-management/src/domain/user/service_tests.rs b/gears/system/account-management/account-management/src/domain/user/service_tests.rs index 035b18989..fb5acd15a 100644 --- a/gears/system/account-management/account-management/src/domain/user/service_tests.rs +++ b/gears/system/account-management/account-management/src/domain/user/service_tests.rs @@ -25,6 +25,7 @@ )] use std::sync::Arc; +use toolkit_gts::gts_id; use account_management_sdk::{ IdpNewUser, IdpUser, IdpUserFilterField, IdpUserPagination, ListUsersQuery, @@ -42,6 +43,8 @@ use crate::domain::tenant::test_support::{FakeTenantRepo, mock_enforcer}; use crate::domain::user::service::UserService; use crate::domain::user::test_support::{FakeIdpUserProvisioner, FakeUserOutcome}; +const USER_SCHEMA: &str = gts_id!("cf.core.am.user.v1~"); + /// Canonical chained `tenant_type` every seeded tenant carries. The /// matching `GtsTypeSchema` is pre-registered in [`make_service`]'s /// `MockTypesRegistryClient` so `UserService::resolve_active_tenant` @@ -50,10 +53,10 @@ use crate::domain::user::test_support::{FakeIdpUserProvisioner, FakeUserOutcome} /// isolation refactor — a missing schema now surfaces as /// `ServiceUnavailable` instead of `Option::None`). Tests that need /// a specific failure shape override the registry stub. -const TEST_TENANT_TYPE_ID: &str = "gts.cf.core.am.tenant_type.v1~cf.core.am.customer.v1~"; +const TEST_TENANT_TYPE_ID: &str = gts_id!("cf.core.am.tenant_type.v1~cf.core.am.customer.v1~"); fn test_tenant_type_uuid() -> Uuid { - gts::GtsID::new(TEST_TENANT_TYPE_ID) + gts::GtsId::try_new(TEST_TENANT_TYPE_ID) .expect("hardcoded chain is valid") .to_uuid() } @@ -120,7 +123,7 @@ fn user_schema() -> GtsTypeSchema { "display_name": { "type": "string", "minLength": 1, "maxLength": 255 }, }, }); - GtsTypeSchema::try_new(GtsTypeId::new("gts.cf.core.am.user.v1~"), body, None, None) + GtsTypeSchema::try_new(GtsTypeId::new(USER_SCHEMA), body, None, None) .expect("synthetic user schema is valid") } @@ -299,7 +302,7 @@ async fn create_user_without_registered_user_schema_returns_service_unavailable( match err { DomainError::ServiceUnavailable { detail, .. } => { assert!( - detail.contains("gts.cf.core.am.user.v1~") && detail.contains("catalog"), + detail.contains(USER_SCHEMA) && detail.contains("catalog"), "ServiceUnavailable.detail must name the missing schema and the \ catalog-seed remediation; got: {detail}" ); @@ -1163,7 +1166,7 @@ mod cleanup { // canonical `NotFound` the real RG ladder emits so the user-cleanup // path's `.map_err(ResourceGroupError::from)` idempotent dispatch is // exercised as in prod. - #[resource_error("gts.cf.core.resource_group.group.v1~")] + #[resource_error(gts_id!("cf.core.resource_group.group.v1~"))] struct RgErr; fn rg_not_found(code: &str) -> CanonicalError { diff --git a/gears/system/account-management/account-management/src/domain/user_groups/cascade_tests.rs b/gears/system/account-management/account-management/src/domain/user_groups/cascade_tests.rs index 04be9b2e7..9bfca5696 100644 --- a/gears/system/account-management/account-management/src/domain/user_groups/cascade_tests.rs +++ b/gears/system/account-management/account-management/src/domain/user_groups/cascade_tests.rs @@ -43,7 +43,7 @@ use crate::domain::tenant::hooks::HookError; // canonical `NotFound` the real RG ladder emits so the cascade hook's // `.map_err(ResourceGroupError::from)` idempotent-NotFound dispatch is // exercised as in prod. -#[resource_error("gts.cf.core.resource_group.group.v1~")] +#[resource_error(gts_id!("cf.core.resource_group.group.v1~"))] struct RgErr; fn rg_not_found(code: &str) -> CanonicalError { diff --git a/gears/system/account-management/account-management/src/domain/user_groups/registration_tests.rs b/gears/system/account-management/account-management/src/domain/user_groups/registration_tests.rs index 7038bf77b..886086e79 100644 --- a/gears/system/account-management/account-management/src/domain/user_groups/registration_tests.rs +++ b/gears/system/account-management/account-management/src/domain/user_groups/registration_tests.rs @@ -31,6 +31,7 @@ use std::collections::HashMap; use std::sync::Arc; use std::sync::Mutex; use std::sync::atomic::{AtomicUsize, Ordering}; +use toolkit_gts::gts_id; use async_trait::async_trait; use resource_group_sdk::{ @@ -50,7 +51,7 @@ use super::{USER_GROUP_TYPE_CODE, USER_MEMBERSHIP_TYPE}; // synthesize the canonical errors the real RG ladder emits, so the // production code's `.map_err(ResourceGroupError::from)` dispatch // (NotFound vs. transport failure) is exercised exactly as in prod. -#[resource_error("gts.cf.core.resource_group.group.v1~")] +#[resource_error(gts_id!("cf.core.resource_group.group.v1~"))] struct RgErr; fn rg_not_found(code: &str) -> CanonicalError { @@ -491,8 +492,8 @@ async fn container_loose_extras_accepted() { // the previous single-type "extras accepted" test, now exercised // against the post-Path-D two-type spec. let row = FakeRgClient::loose_container_row( - &["gts.cf.core.rg.type.v1~extra.type.v1~"], - &["gts.cf.core.rg.type.v1~extra.member.v1~"], + &[gts_id!("cf.core.rg.type.v1~extra.rg.test.type.v1~")], + &[gts_id!("cf.core.rg.type.v1~extra.rg.test.member.v1~")], ); let client = FakeRgClient::defaults().with(USER_GROUP_TYPE_CODE, TypeState::already_present(row)); diff --git a/gears/system/account-management/account-management/src/gear_tests.rs b/gears/system/account-management/account-management/src/gear_tests.rs index 2273ef319..12b8c5ed9 100644 --- a/gears/system/account-management/account-management/src/gear_tests.rs +++ b/gears/system/account-management/account-management/src/gear_tests.rs @@ -10,6 +10,7 @@ )] use std::sync::Arc; +use toolkit_gts::gts_id; use tokio_util::sync::CancellationToken; use uuid::Uuid; @@ -28,6 +29,9 @@ use crate::domain::tenant::test_support::{ use crate::domain::bootstrap::BootstrapService; use crate::domain::tenant::TenantRepo; +const TENANT_SCHEMA: &str = gts_id!("cf.core.am.tenant.v1~"); +const TENANT_TYPE_SCHEMA: &str = gts_id!("cf.core.am.tenant_type.v1~"); + /// Test-only helper that combines bootstrap config validation + saga /// execution in one call, mirroring the split init/serve production /// flow. Generic over `R: TenantRepo` so tests can pass @@ -192,7 +196,7 @@ async fn stateful_task_shuts_down_on_cancel() { // + `FakeTenantRepo` infra; no `BootstrapService` mocking required. // --------------------------------------------------------------------- -const ROOT_TENANT_TYPE: &str = "gts.cf.core.am.tenant_type.v1~cf.core.am.platform.v1~"; +const ROOT_TENANT_TYPE: &str = gts_id!("cf.core.am.tenant_type.v1~cf.core.am.platform.v1~"); fn root_id() -> Uuid { Uuid::from_u128(0x100) @@ -410,9 +414,9 @@ fn stub_types_registry() -> Arc { // `validate_tenant_name_via_gts` rejects `root_name` and // the "valid config + CleanFailure" path never reaches // `FakeOutcome::CleanFailure`. - let (id, body) = if type_id == "gts.cf.core.am.tenant.v1~" { + let (id, body) = if type_id == TENANT_SCHEMA { ( - "gts.cf.core.am.tenant.v1~", + TENANT_SCHEMA, serde_json::json!({ "type": "object", "properties": { @@ -421,7 +425,7 @@ fn stub_types_registry() -> Arc { }), ) } else { - ("gts.cf.core.am.tenant_type.v1~", serde_json::json!({})) + (TENANT_TYPE_SCHEMA, serde_json::json!({})) }; Ok(GtsTypeSchema::try_new(GtsTypeId::new(id), body, None, None) .expect("canned root schema must construct")) diff --git a/gears/system/account-management/account-management/src/gts/permissions.rs b/gears/system/account-management/account-management/src/gts/permissions.rs index ad11a33be..9b971f102 100644 --- a/gears/system/account-management/account-management/src/gts/permissions.rs +++ b/gears/system/account-management/account-management/src/gts/permissions.rs @@ -29,7 +29,7 @@ use crate::domain::user::service::pep::actions as user_actions; gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.core.am.tenant_create.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.core.am.tenant_create.v1"), resource_type: TENANT_RESOURCE_TYPE.to_owned(), action: tenant_actions::CREATE.to_owned(), display_name: "Create tenant".to_owned(), @@ -37,7 +37,7 @@ gts_instance! { } gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.core.am.tenant_read.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.core.am.tenant_read.v1"), resource_type: TENANT_RESOURCE_TYPE.to_owned(), action: tenant_actions::READ.to_owned(), display_name: "Read tenant".to_owned(), @@ -45,7 +45,7 @@ gts_instance! { } gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.core.am.tenant_update.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.core.am.tenant_update.v1"), resource_type: TENANT_RESOURCE_TYPE.to_owned(), action: tenant_actions::UPDATE.to_owned(), display_name: "Update tenant".to_owned(), @@ -53,7 +53,7 @@ gts_instance! { } gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.core.am.tenant_delete.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.core.am.tenant_delete.v1"), resource_type: TENANT_RESOURCE_TYPE.to_owned(), action: tenant_actions::DELETE.to_owned(), display_name: "Delete tenant".to_owned(), @@ -61,7 +61,7 @@ gts_instance! { } gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.core.am.tenant_list_children.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.core.am.tenant_list_children.v1"), resource_type: TENANT_RESOURCE_TYPE.to_owned(), action: tenant_actions::LIST_CHILDREN.to_owned(), display_name: "List child tenants".to_owned(), @@ -72,7 +72,7 @@ gts_instance! { gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.core.am.tenant_metadata_read.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.core.am.tenant_metadata_read.v1"), resource_type: TENANT_METADATA_RESOURCE_TYPE.to_owned(), action: metadata_actions::READ.to_owned(), display_name: "Read tenant metadata".to_owned(), @@ -80,7 +80,7 @@ gts_instance! { } gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.core.am.tenant_metadata_write.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.core.am.tenant_metadata_write.v1"), resource_type: TENANT_METADATA_RESOURCE_TYPE.to_owned(), action: metadata_actions::WRITE.to_owned(), display_name: "Write tenant metadata".to_owned(), @@ -88,7 +88,7 @@ gts_instance! { } gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.core.am.tenant_metadata_list.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.core.am.tenant_metadata_list.v1"), resource_type: TENANT_METADATA_RESOURCE_TYPE.to_owned(), action: metadata_actions::LIST.to_owned(), display_name: "List tenant metadata".to_owned(), @@ -96,7 +96,7 @@ gts_instance! { } gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.core.am.tenant_metadata_delete.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.core.am.tenant_metadata_delete.v1"), resource_type: TENANT_METADATA_RESOURCE_TYPE.to_owned(), action: metadata_actions::DELETE.to_owned(), display_name: "Delete tenant metadata".to_owned(), @@ -107,7 +107,7 @@ gts_instance! { gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.core.am.conversion_request.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.core.am.conversion_request.v1"), resource_type: CONVERSION_REQUEST_RESOURCE_TYPE.to_owned(), action: conversion_actions::REQUEST.to_owned(), display_name: "Request tenant conversion".to_owned(), @@ -115,7 +115,7 @@ gts_instance! { } gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.core.am.conversion_cancel.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.core.am.conversion_cancel.v1"), resource_type: CONVERSION_REQUEST_RESOURCE_TYPE.to_owned(), action: conversion_actions::CANCEL.to_owned(), display_name: "Cancel conversion request".to_owned(), @@ -123,7 +123,7 @@ gts_instance! { } gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.core.am.conversion_reject.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.core.am.conversion_reject.v1"), resource_type: CONVERSION_REQUEST_RESOURCE_TYPE.to_owned(), action: conversion_actions::REJECT.to_owned(), display_name: "Reject conversion request".to_owned(), @@ -131,7 +131,7 @@ gts_instance! { } gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.core.am.conversion_approve.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.core.am.conversion_approve.v1"), resource_type: CONVERSION_REQUEST_RESOURCE_TYPE.to_owned(), action: conversion_actions::APPROVE.to_owned(), display_name: "Approve conversion request".to_owned(), @@ -139,7 +139,7 @@ gts_instance! { } gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.core.am.conversion_list_own.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.core.am.conversion_list_own.v1"), resource_type: CONVERSION_REQUEST_RESOURCE_TYPE.to_owned(), action: conversion_actions::LIST_OWN.to_owned(), display_name: "List own conversion requests".to_owned(), @@ -147,7 +147,7 @@ gts_instance! { } gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.core.am.conversion_list_inbound.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.core.am.conversion_list_inbound.v1"), resource_type: CONVERSION_REQUEST_RESOURCE_TYPE.to_owned(), action: conversion_actions::LIST_INBOUND.to_owned(), display_name: "List inbound conversion requests".to_owned(), @@ -158,7 +158,7 @@ gts_instance! { gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.core.am.user_create.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.core.am.user_create.v1"), resource_type: USER_RESOURCE_TYPE.to_owned(), action: user_actions::CREATE.to_owned(), display_name: "Create user".to_owned(), @@ -166,7 +166,7 @@ gts_instance! { } gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.core.am.user_list.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.core.am.user_list.v1"), resource_type: USER_RESOURCE_TYPE.to_owned(), action: user_actions::LIST.to_owned(), display_name: "List users".to_owned(), @@ -174,7 +174,7 @@ gts_instance! { } gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.core.am.user_delete.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.core.am.user_delete.v1"), resource_type: USER_RESOURCE_TYPE.to_owned(), action: user_actions::DELETE.to_owned(), display_name: "Delete user".to_owned(), @@ -183,9 +183,9 @@ gts_instance! { #[cfg(test)] mod tests { - use toolkit_gts::InventoryInstance; + use toolkit_gts::{InventoryInstance, gts_id}; - const PERMISSION_TYPE_ID: &str = "gts.cf.toolkit.authz.permission.v1~"; + const PERMISSION_TYPE_ID: &str = gts_id!("cf.toolkit.authz.permission.v1~"); /// AM's instance-id namespace segment, appended after /// [`PERMISSION_TYPE_ID`]. Kept as a bare fragment (not a `gts.`-prefixed /// literal) so it is composed with the valid type id at the filter site @@ -194,24 +194,24 @@ mod tests { /// One per `(resource_type, action)` the AM REST/PEP surface enforces. const EXPECTED_PERMISSION_IDS: &[&str] = &[ - "gts.cf.toolkit.authz.permission.v1~cf.core.am.tenant_create.v1", - "gts.cf.toolkit.authz.permission.v1~cf.core.am.tenant_read.v1", - "gts.cf.toolkit.authz.permission.v1~cf.core.am.tenant_update.v1", - "gts.cf.toolkit.authz.permission.v1~cf.core.am.tenant_delete.v1", - "gts.cf.toolkit.authz.permission.v1~cf.core.am.tenant_list_children.v1", - "gts.cf.toolkit.authz.permission.v1~cf.core.am.tenant_metadata_read.v1", - "gts.cf.toolkit.authz.permission.v1~cf.core.am.tenant_metadata_write.v1", - "gts.cf.toolkit.authz.permission.v1~cf.core.am.tenant_metadata_list.v1", - "gts.cf.toolkit.authz.permission.v1~cf.core.am.tenant_metadata_delete.v1", - "gts.cf.toolkit.authz.permission.v1~cf.core.am.conversion_request.v1", - "gts.cf.toolkit.authz.permission.v1~cf.core.am.conversion_cancel.v1", - "gts.cf.toolkit.authz.permission.v1~cf.core.am.conversion_reject.v1", - "gts.cf.toolkit.authz.permission.v1~cf.core.am.conversion_approve.v1", - "gts.cf.toolkit.authz.permission.v1~cf.core.am.conversion_list_own.v1", - "gts.cf.toolkit.authz.permission.v1~cf.core.am.conversion_list_inbound.v1", - "gts.cf.toolkit.authz.permission.v1~cf.core.am.user_create.v1", - "gts.cf.toolkit.authz.permission.v1~cf.core.am.user_list.v1", - "gts.cf.toolkit.authz.permission.v1~cf.core.am.user_delete.v1", + gts_id!("cf.toolkit.authz.permission.v1~cf.core.am.tenant_create.v1"), + gts_id!("cf.toolkit.authz.permission.v1~cf.core.am.tenant_read.v1"), + gts_id!("cf.toolkit.authz.permission.v1~cf.core.am.tenant_update.v1"), + gts_id!("cf.toolkit.authz.permission.v1~cf.core.am.tenant_delete.v1"), + gts_id!("cf.toolkit.authz.permission.v1~cf.core.am.tenant_list_children.v1"), + gts_id!("cf.toolkit.authz.permission.v1~cf.core.am.tenant_metadata_read.v1"), + gts_id!("cf.toolkit.authz.permission.v1~cf.core.am.tenant_metadata_write.v1"), + gts_id!("cf.toolkit.authz.permission.v1~cf.core.am.tenant_metadata_list.v1"), + gts_id!("cf.toolkit.authz.permission.v1~cf.core.am.tenant_metadata_delete.v1"), + gts_id!("cf.toolkit.authz.permission.v1~cf.core.am.conversion_request.v1"), + gts_id!("cf.toolkit.authz.permission.v1~cf.core.am.conversion_cancel.v1"), + gts_id!("cf.toolkit.authz.permission.v1~cf.core.am.conversion_reject.v1"), + gts_id!("cf.toolkit.authz.permission.v1~cf.core.am.conversion_approve.v1"), + gts_id!("cf.toolkit.authz.permission.v1~cf.core.am.conversion_list_own.v1"), + gts_id!("cf.toolkit.authz.permission.v1~cf.core.am.conversion_list_inbound.v1"), + gts_id!("cf.toolkit.authz.permission.v1~cf.core.am.user_create.v1"), + gts_id!("cf.toolkit.authz.permission.v1~cf.core.am.user_list.v1"), + gts_id!("cf.toolkit.authz.permission.v1~cf.core.am.user_delete.v1"), ]; fn am_permission_instances() -> Vec<&'static InventoryInstance> { diff --git a/gears/system/account-management/account-management/src/infra/idp/lazy_tests.rs b/gears/system/account-management/account-management/src/infra/idp/lazy_tests.rs index b504eac68..24f146057 100644 --- a/gears/system/account-management/account-management/src/infra/idp/lazy_tests.rs +++ b/gears/system/account-management/account-management/src/infra/idp/lazy_tests.rs @@ -19,6 +19,7 @@ //! timing the wrapper exists to handle. use std::sync::Arc; +use toolkit_gts::gts_id; use account_management_sdk::{IdpPluginClient, IdpPluginSpecV1, IdpProvisionTenantRequest}; use gts::GtsTypeId; @@ -53,7 +54,7 @@ fn build_instance( /// test stays within the upstream-shared GTS namespace (no /// deployment-specific identifiers leak into `gears-rust`). fn request() -> IdpProvisionTenantRequest { - let tenant_type = GtsTypeId::new("gts.cf.core.am.tenant_type.v1~cf.core.am.customer.v1~"); + let tenant_type = GtsTypeId::new(gts_id!("cf.core.am.tenant_type.v1~cf.core.am.customer.v1~")); IdpProvisionTenantRequest::new( Uuid::from_u128(0xC11D), Uuid::from_u128(0xBEEF), diff --git a/gears/system/account-management/account-management/src/infra/idp/noop_tests.rs b/gears/system/account-management/account-management/src/infra/idp/noop_tests.rs index d2963150c..ccc1d7aa1 100644 --- a/gears/system/account-management/account-management/src/infra/idp/noop_tests.rs +++ b/gears/system/account-management/account-management/src/infra/idp/noop_tests.rs @@ -4,6 +4,7 @@ use account_management_sdk::{ IdpListUsersRequest, IdpNewUser, IdpProvisionFailure, IdpProvisionTenantRequest, IdpProvisionUserRequest, IdpTenantContext, IdpUserOperationFailure, IdpUserPagination, }; +use toolkit_gts::gts_id; use toolkit_security::SecurityContext; use uuid::Uuid; @@ -11,7 +12,7 @@ fn sample_tenant_context() -> IdpTenantContext { IdpTenantContext::new( Uuid::nil(), "t", - gts::GtsTypeId::new("gts.cf.core.am.tenant_type.v1~cf.core.am.customer.v1~"), + gts::GtsTypeId::new(gts_id!("cf.core.am.tenant_type.v1~cf.core.am.customer.v1~")), None, ) } @@ -26,7 +27,7 @@ async fn noop_provider_reports_unsupported_operation_on_provision_tenant() { let req = IdpProvisionTenantRequest::for_root( Uuid::nil(), "t", - gts::GtsTypeId::new("gts.cf.core.am.tenant_type.v1~cf.core.am.customer.v1~"), + gts::GtsTypeId::new(gts_id!("cf.core.am.tenant_type.v1~cf.core.am.customer.v1~")), ); let err = p .provision_tenant(&sec_ctx(), &req) diff --git a/gears/system/account-management/account-management/src/infra/rg/checker_tests.rs b/gears/system/account-management/account-management/src/infra/rg/checker_tests.rs index 9ce20149e..d2fd615a6 100644 --- a/gears/system/account-management/account-management/src/infra/rg/checker_tests.rs +++ b/gears/system/account-management/account-management/src/infra/rg/checker_tests.rs @@ -13,6 +13,7 @@ use resource_group_sdk::{ }; use std::sync::Mutex; use toolkit_canonical_errors::CanonicalError; +use toolkit_gts::gts_id; use toolkit_odata::Page; #[allow( @@ -70,7 +71,7 @@ impl FakeRgClient { fn sample_group(tenant_id: Uuid) -> ResourceGroup { ResourceGroup { id: Uuid::from_u128(0xDEAD), - code: "gts.cf.core.rg.type.v1~acme.rg.test.example.v1~".into(), + code: gts_id!("cf.core.rg.type.v1~acme.rg.test.example.v1~").into(), name: "sample".into(), hierarchy: GroupHierarchy { parent_id: None, diff --git a/gears/system/account-management/account-management/src/infra/sdk_error_mapping.rs b/gears/system/account-management/account-management/src/infra/sdk_error_mapping.rs index 4238b20ac..818e54b85 100644 --- a/gears/system/account-management/account-management/src/infra/sdk_error_mapping.rs +++ b/gears/system/account-management/account-management/src/infra/sdk_error_mapping.rs @@ -52,10 +52,10 @@ use crate::domain::metrics::{AM_CROSS_TENANT_DENIAL, MetricKind, emit_metric}; // Resource markers — kept in sync with account_management_sdk::gts. // --------------------------------------------------------------------------- -#[resource_error("gts.cf.core.am.tenant.v1~")] +#[resource_error(gts_id!("cf.core.am.tenant.v1~"))] pub(crate) struct TenantResource; -#[resource_error("gts.cf.core.am.user.v1~")] +#[resource_error(gts_id!("cf.core.am.user.v1~"))] pub(crate) struct UserResource; // `TenantMetadataResource` carries the unified 404 for the metadata @@ -64,10 +64,10 @@ pub(crate) struct UserResource; // `type_id` the caller supplied is surfaced through `resource_name`, // so consumers still see *which* schema was involved without a separate // type-level discriminator. -#[resource_error("gts.cf.core.am.tenant_metadata.v1~")] +#[resource_error(gts_id!("cf.core.am.tenant_metadata.v1~"))] pub(crate) struct TenantMetadataResource; -#[resource_error("gts.cf.core.am.conversion_request.v1~")] +#[resource_error(gts_id!("cf.core.am.conversion_request.v1~"))] pub(crate) struct ConversionRequestResource; // --------------------------------------------------------------------------- diff --git a/gears/system/account-management/account-management/src/infra/sdk_error_mapping_tests.rs b/gears/system/account-management/account-management/src/infra/sdk_error_mapping_tests.rs index dd5c7782d..b3d36a9e8 100644 --- a/gears/system/account-management/account-management/src/infra/sdk_error_mapping_tests.rs +++ b/gears/system/account-management/account-management/src/infra/sdk_error_mapping_tests.rs @@ -9,6 +9,7 @@ //! `tests/api_status_mapping_test.rs`. use std::time::Duration; +use toolkit_gts::gts_id; use toolkit_canonical_errors::{CanonicalError, InvalidArgument}; @@ -236,7 +237,7 @@ fn metadata_entry_not_found_uses_metadata_resource_type_with_chained_type_id_as_ // `TENANT_METADATA_RESOURCE_TYPE` (`gts.cf.core.am.tenant_metadata.v1~`) // with the chained `type_id` the caller supplied as // `resource_name`. - let chain = "gts.cf.core.am.tenant_metadata.v1~cf.core.billing.usage.v1~"; + let chain = gts_id!("cf.core.am.tenant_metadata.v1~cf.core.billing.usage.v1~"); let canonical = round_trip(DomainError::MetadataEntryNotFound { detail: "entry missing".to_owned(), entry: chain.to_owned(), @@ -292,7 +293,7 @@ fn metadata_version_mismatch_maps_to_aborted_409_with_reason() { // conflict from a generic 409, and a future mapper drift that // dropped `with_reason` would change the wire envelope in a way // unit-tested ONLY here. - let chain = "gts.cf.core.am.tenant_metadata.v1~cf.core.billing.usage.v1~"; + let chain = gts_id!("cf.core.am.tenant_metadata.v1~cf.core.billing.usage.v1~"); let canonical = round_trip(DomainError::MetadataVersionMismatch { entry: chain.to_owned(), expected: 4, diff --git a/gears/system/account-management/account-management/src/infra/storage/repo_impl/reads.rs b/gears/system/account-management/account-management/src/infra/storage/repo_impl/reads.rs index a07474a4e..3a993e089 100644 --- a/gears/system/account-management/account-management/src/infra/storage/repo_impl/reads.rs +++ b/gears/system/account-management/account-management/src/infra/storage/repo_impl/reads.rs @@ -9,7 +9,7 @@ use std::collections::HashMap; use account_management_sdk::TenantInfoFilterField; use bigdecimal::BigDecimal; -use gts::GtsID; +use gts::GtsId; use sea_orm::sea_query::Expr; use sea_orm::{ColumnTrait, Condition, EntityTrait, FromQueryResult, Order, QuerySelect}; use serde_json::Value; @@ -109,7 +109,7 @@ impl FieldToColumn for TenantODataMapper { } // Chained `gts.*` tenant-type string → its deterministic UUIDv5, // compared against the `tenant_type_uuid` column. Same derivation - // as `uuid_for_registered_schema` (`GtsID::new(s).to_uuid()`), so a + // as `uuid_for_registered_schema` (`GtsId::try_new(s).to_uuid()`), so a // value taken from the projection round-trips to the stored UUID. // Only membership operators are admissible — an ordered comparison // on a derived UUID has no honest meaning (mirrors `status`). @@ -124,7 +124,7 @@ impl FieldToColumn for TenantODataMapper { )); } } - let uuid = GtsID::new(s).map(|g| g.to_uuid()).map_err(|e| { + let uuid = GtsId::try_new(s).map(|g| g.to_uuid()).map_err(|e| { format!( "invalid `tenant_type` value '{s}'; expected a chained GTS \ type id (e.g. 'gts.cf.core.am.tenant_type.v1~…~'): {e}" @@ -588,11 +588,13 @@ pub(super) async fn is_descendant( #[cfg(test)] mod tenant_type_filter_tests { use super::*; + use toolkit_gts::gts_id; // A valid `cf`-vendor chained tenant-type id (the one `service_tests` // uses). de0901 validates GTS string literals via `GtsOps::parse_id`; a // valid `cf` id is accepted, so no lint suppression is needed. - const SAMPLE_TENANT_TYPE_GTS: &str = "gts.cf.core.am.tenant_type.v1~cf.core.am.customer.v1~"; + const SAMPLE_TENANT_TYPE_GTS: &str = + gts_id!("cf.core.am.tenant_type.v1~cf.core.am.customer.v1~"); type Mapper = TenantODataMapper; type Field = TenantInfoFilterField; @@ -600,10 +602,12 @@ mod tenant_type_filter_tests { /// `$filter=tenant_type eq ''` maps the chained string to the same /// deterministic `UUIDv5` the storage column holds, so a value /// taken from the projection round-trips. Pins the derivation against - /// `GtsID::to_uuid` so a future codec change is caught. + /// `GtsId::to_uuid` so a future codec change is caught. #[test] fn tenant_type_string_maps_to_derived_uuidv5() { - let expected = GtsID::new(SAMPLE_TENANT_TYPE_GTS).expect("gts").to_uuid(); + let expected = GtsId::try_new(SAMPLE_TENANT_TYPE_GTS) + .expect("gts") + .to_uuid(); let out = >::map_value( Field::TenantType, FilterOp::Eq, diff --git a/gears/system/account-management/account-management/src/infra/types_registry/checker.rs b/gears/system/account-management/account-management/src/infra/types_registry/checker.rs index 6c204cb4f..75550a3d4 100644 --- a/gears/system/account-management/account-management/src/infra/types_registry/checker.rs +++ b/gears/system/account-management/account-management/src/infra/types_registry/checker.rs @@ -34,6 +34,7 @@ use std::sync::Arc; use std::time::Duration; +use toolkit_gts::gts_id; use async_trait::async_trait; use serde_json::Value; @@ -50,7 +51,7 @@ use crate::domain::tenant_type::checker::TenantTypeChecker; /// `allowed_parent_types` trait — they're rejected as /// [`DomainError::InvalidTenantType`] regardless of what the /// effective-trait merge produces. -const TENANT_TYPE_BASE_GTS_ID: &str = "gts.cf.core.am.tenant_type.v1~"; +const TENANT_TYPE_BASE_GTS_ID: &str = gts_id!("cf.core.am.tenant_type.v1~"); /// Top-level key on the effective trait map carrying the list of /// admitted parent GTS identifiers. Defined on diff --git a/gears/system/account-management/account-management/src/infra/types_registry/checker_tests.rs b/gears/system/account-management/account-management/src/infra/types_registry/checker_tests.rs index 051d99142..c7f23ff3d 100644 --- a/gears/system/account-management/account-management/src/infra/types_registry/checker_tests.rs +++ b/gears/system/account-management/account-management/src/infra/types_registry/checker_tests.rs @@ -10,6 +10,7 @@ use super::*; use serde_json::json; use std::collections::HashMap; use std::sync::Mutex; +use toolkit_gts::{GTS_ID_PREFIX, gts_id, gts_uri}; use types_registry_sdk::{GtsInstance, GtsTypeId, InstanceQuery, RegisterResult, TypeSchemaQuery}; /// Build a `GtsTypeSchema` for testing. `traits_schema` carries @@ -21,7 +22,7 @@ fn schema( own_allowed_parents: Option>, ) -> Arc { let mut raw = json!({ - "$id": format!("gts://{type_id}"), + "$id": gts_uri!(type_id), "type": "object", }); if let Some(parents) = own_allowed_parents { @@ -39,7 +40,7 @@ fn schema( /// to `[]`, mirroring `docs/schemas/tenant_type.v1.schema.json`. fn envelope() -> Arc { let raw = json!({ - "$id": format!("gts://{TENANT_TYPE_BASE_GTS_ID}"), + "$id": gts_uri!(TENANT_TYPE_BASE_GTS_ID), "type": "object", "x-gts-traits-schema": { "type": "object", @@ -177,16 +178,16 @@ impl TypesRegistryClient for FakeRegistry { async fn admits_when_parent_in_child_allowed_parent_types() { let envelope = envelope(); let parent_schema = schema( - "gts.cf.core.am.tenant_type.v1~acme.am.tenant_type.platform.v1~", + gts_id!("cf.core.am.tenant_type.v1~acme.am.tenant_type.platform.v1~"), Some(envelope.clone()), None, ); let child_schema = schema( - "gts.cf.core.am.tenant_type.v1~acme.am.tenant_type.customer.v1~", + gts_id!("cf.core.am.tenant_type.v1~acme.am.tenant_type.customer.v1~"), Some(envelope), - Some(vec![ - "gts.cf.core.am.tenant_type.v1~acme.am.tenant_type.platform.v1~", - ]), + Some(vec![gts_id!( + "cf.core.am.tenant_type.v1~acme.am.tenant_type.platform.v1~" + )]), ); let parent_uuid = parent_schema.type_uuid; let child_uuid = child_schema.type_uuid; @@ -217,12 +218,12 @@ async fn admits_when_parent_in_child_allowed_parent_types() { async fn admits_same_type_nesting_when_self_listed() { let envelope = envelope(); let nested = schema( - "gts.cf.core.am.tenant_type.v1~acme.am.tenant_type.partner.v1~", + gts_id!("cf.core.am.tenant_type.v1~acme.am.tenant_type.partner.v1~"), Some(envelope), // Self-reference admits same-type nesting. - Some(vec![ - "gts.cf.core.am.tenant_type.v1~acme.am.tenant_type.partner.v1~", - ]), + Some(vec![gts_id!( + "cf.core.am.tenant_type.v1~acme.am.tenant_type.partner.v1~" + )]), ); let nested_uuid = nested.type_uuid; let registry = Arc::new(FakeRegistry::new(vec![( @@ -240,12 +241,12 @@ async fn admits_same_type_nesting_when_self_listed() { async fn rejects_same_type_nesting_when_self_not_listed() { let envelope = envelope(); let leaf = schema( - "gts.cf.core.am.tenant_type.v1~acme.am.tenant_type.customer.v1~", + gts_id!("cf.core.am.tenant_type.v1~acme.am.tenant_type.customer.v1~"), Some(envelope), // Self NOT in the list → same-type nesting disallowed. - Some(vec![ - "gts.cf.core.am.tenant_type.v1~acme.am.tenant_type.platform.v1~", - ]), + Some(vec![gts_id!( + "cf.core.am.tenant_type.v1~acme.am.tenant_type.platform.v1~" + )]), ); let leaf_uuid = leaf.type_uuid; let registry = Arc::new(FakeRegistry::new(vec![( @@ -267,16 +268,16 @@ async fn rejects_same_type_nesting_when_self_not_listed() { async fn rejects_when_parent_not_in_allowed_list() { let envelope = envelope(); let parent_schema = schema( - "gts.cf.core.am.tenant_type.v1~acme.am.tenant_type.stranger.v1~", + gts_id!("cf.core.am.tenant_type.v1~acme.am.tenant_type.stranger.v1~"), Some(envelope.clone()), None, ); let child_schema = schema( - "gts.cf.core.am.tenant_type.v1~acme.am.tenant_type.customer.v1~", + gts_id!("cf.core.am.tenant_type.v1~acme.am.tenant_type.customer.v1~"), Some(envelope), - Some(vec![ - "gts.cf.core.am.tenant_type.v1~acme.am.tenant_type.platform.v1~", - ]), + Some(vec![gts_id!( + "cf.core.am.tenant_type.v1~acme.am.tenant_type.platform.v1~" + )]), ); let parent_uuid = parent_schema.type_uuid; let child_uuid = child_schema.type_uuid; @@ -299,9 +300,9 @@ async fn rejects_child_not_under_envelope_as_invalid_tenant_type() { // Schema is registered, but its chain root is not the AM // tenant_type envelope — every membership check would be against // a meaningless trait map. - let alien_root = schema("gts.acme.core.events.type.v1~", None, None); + let alien_root = schema(gts_id!("acme.core.events.type.v1~"), None, None); let alien_leaf = schema( - "gts.acme.core.events.type.v1~acme.commerce.orders.order.v1~", + gts_id!("acme.core.events.type.v1~acme.commerce.orders.order.v1~"), Some(alien_root), Some(vec!["whatever"]), ); @@ -312,7 +313,7 @@ async fn rejects_child_not_under_envelope_as_invalid_tenant_type() { ( parent, Ok(GtsTypeSchema::try_new( - GtsTypeId::new("gts.acme.core.events.type.v1~"), + GtsTypeId::new(gts_id!("acme.core.events.type.v1~")), json!({"type": "object"}), None, None, @@ -340,20 +341,20 @@ async fn rejects_parent_not_under_envelope_as_invalid_tenant_type() { // would still match and admit a non-tenant-type parent; the // envelope re-check turns this into a clear `InvalidTenantType`. let envelope = envelope(); - let alien_root = schema("gts.acme.core.events.type.v1~", None, None); + let alien_root = schema(gts_id!("acme.core.events.type.v1~"), None, None); let alien_parent = schema( - "gts.acme.core.events.type.v1~acme.commerce.orders.order.v1~", + gts_id!("acme.core.events.type.v1~acme.commerce.orders.order.v1~"), Some(alien_root), None, ); let child_schema = schema( - "gts.cf.core.am.tenant_type.v1~acme.am.tenant_type.customer.v1~", + gts_id!("cf.core.am.tenant_type.v1~acme.am.tenant_type.customer.v1~"), Some(envelope), // Deliberately list the alien parent's id — without the // envelope re-check the membership would match and admit. - Some(vec![ - "gts.acme.core.events.type.v1~acme.commerce.orders.order.v1~", - ]), + Some(vec![gts_id!( + "acme.core.events.type.v1~acme.commerce.orders.order.v1~" + )]), ); let parent_uuid = alien_parent.type_uuid; let child_uuid = child_schema.type_uuid; @@ -397,11 +398,11 @@ async fn rejects_child_not_registered_as_invalid_tenant_type() { async fn rejects_parent_not_registered_as_invalid_tenant_type() { let envelope = envelope(); let child_schema = schema( - "gts.cf.core.am.tenant_type.v1~acme.am.tenant_type.customer.v1~", + gts_id!("cf.core.am.tenant_type.v1~acme.am.tenant_type.customer.v1~"), Some(envelope), - Some(vec![ - "gts.cf.core.am.tenant_type.v1~acme.am.tenant_type.platform.v1~", - ]), + Some(vec![gts_id!( + "cf.core.am.tenant_type.v1~acme.am.tenant_type.platform.v1~" + )]), ); let child_uuid = child_schema.type_uuid; let parent_uuid = Uuid::from_u128(0xCAFE); @@ -439,17 +440,19 @@ async fn rejects_when_allowed_parent_types_contains_non_tenant_type_chained_id() // an otherwise legit type-shape entry. The whole list must // collapse to malformed. let raw = json!({ - "$id": "gts://gts.cf.core.am.tenant_type.v1~acme.am.tenant_type.child.v1~", + "$id": gts_uri!("cf.core.am.tenant_type.v1~acme.am.tenant_type.child.v1~"), "type": "object", "x-gts-traits": { ALLOWED_PARENT_TYPES_TRAIT: [ - "gts.cf.core.am.tenant_type.v1~acme.am.tenant_type.parent.v1~", - "gts.cf.core.am.tenant_type.v1~acme.am.customer.v1~prod-acct-42", + gts_id!("cf.core.am.tenant_type.v1~acme.am.tenant_type.parent.v1~"), + format!("{GTS_ID_PREFIX}cf.core.am.tenant_type.v1~acme.am.tenant.customer.v1~prod_acct_42"), ], }, }); let child = GtsTypeSchema::try_new( - GtsTypeId::new("gts.cf.core.am.tenant_type.v1~acme.am.tenant_type.child.v1~"), + GtsTypeId::new(gts_id!( + "cf.core.am.tenant_type.v1~acme.am.tenant_type.child.v1~" + )), raw, None, Some(envelope), @@ -475,14 +478,16 @@ async fn rejects_when_allowed_parent_types_is_not_an_array() { // overriding the envelope's `default: []`. let envelope = envelope(); let raw = json!({ - "$id": "gts://gts.cf.core.am.tenant_type.v1~acme.am.tenant_type.broken.v1~", + "$id": gts_uri!("cf.core.am.tenant_type.v1~acme.am.tenant_type.broken.v1~"), "type": "object", "x-gts-traits": { ALLOWED_PARENT_TYPES_TRAIT: "not an array", }, }); let broken = GtsTypeSchema::try_new( - GtsTypeId::new("gts.cf.core.am.tenant_type.v1~acme.am.tenant_type.broken.v1~"), + GtsTypeId::new(gts_id!( + "cf.core.am.tenant_type.v1~acme.am.tenant_type.broken.v1~" + )), raw, None, Some(envelope), diff --git a/gears/system/account-management/account-management/src/infra/types_registry/metadata_schema_registry.rs b/gears/system/account-management/account-management/src/infra/types_registry/metadata_schema_registry.rs index 674aa2d53..800619689 100644 --- a/gears/system/account-management/account-management/src/infra/types_registry/metadata_schema_registry.rs +++ b/gears/system/account-management/account-management/src/infra/types_registry/metadata_schema_registry.rs @@ -25,11 +25,10 @@ //! updates take effect immediately. The SDK's local-client cache is //! responsible for any short-lived caching it chooses to do internally. -use std::sync::Arc; - use async_trait::async_trait; -use gts::{GtsID, GtsTypeId}; +use gts::{GtsId, GtsTypeId}; use serde_json::Value; +use std::sync::Arc; use toolkit_canonical_errors::CanonicalError; use types_registry_sdk::TypesRegistryClient; use uuid::Uuid; @@ -86,7 +85,7 @@ fn map_registry_err(err: CanonicalError, schema_token: &str) -> DomainError { /// Compute the deterministic `schema_uuid` for an already-validated /// `type_id` string. Mirrors the helper in /// [`crate::domain::metadata::registry`] — both rely on -/// `gts::GtsID::to_uuid()` for the canonical namespace. +/// `gts::GtsId::to_uuid()` for the canonical namespace. /// /// # Panics /// @@ -100,7 +99,7 @@ fn map_registry_err(err: CanonicalError, schema_token: &str) -> DomainError { an unparseable input here is a service-layer contract break" )] fn uuid_for_registered_schema(type_id: &str) -> Uuid { - GtsID::new(type_id) + GtsId::try_new(type_id) .expect( "types-registry returned a type_id that does not parse as a GTS id - \ upstream SDK contract break", @@ -249,7 +248,7 @@ impl MetadataSchemaRegistry for GtsMetadataSchemaRegistry { let resolved = schema.effective_schema(); // The base AM metadata envelope declares `$schema: // "http://json-schema.org/draft-07/schema#"`, and derived metadata - // schemas carry `$schema: "gts://gts.cf.core.am.tenant_metadata.v1~"` + // schemas carry `$schema: gts_uri!("cf.core.am.tenant_metadata.v1~")` // -- the GTS chain identifier that AM uses for inheritance. The // generic `jsonschema::validator_for` auto-detects the draft from // `$schema` and fails closed when the URI is not a registered diff --git a/gears/system/account-management/account-management/src/tr_plugin/tests.rs b/gears/system/account-management/account-management/src/tr_plugin/tests.rs index b1ac90eb2..7a87c97b2 100644 --- a/gears/system/account-management/account-management/src/tr_plugin/tests.rs +++ b/gears/system/account-management/account-management/src/tr_plugin/tests.rs @@ -19,6 +19,7 @@ use std::collections::{HashMap, HashSet}; use std::sync::{Arc, Mutex}; +use toolkit_gts::gts_id; use async_trait::async_trait; use tenant_resolver_sdk::{ @@ -51,7 +52,7 @@ const DELETED: TenantStatus = TenantStatus::Deleted; // ── Registry stubs ──────────────────────────────────────────────────────── -const TEST_TYPE_ID: &str = "gts.cf.core.test.tenant.v1~"; +const TEST_TYPE_ID: &str = gts_id!("cf.core.test.tenant.v1~"); /// Dual-mode stub: when `fail=false` it returns a fixed `GtsTypeSchema` for /// any UUID; when `fail=true` every lookup returns `CanonicalError::NotFound`. diff --git a/gears/system/account-management/account-management/tests/api_conversions_test.rs b/gears/system/account-management/account-management/tests/api_conversions_test.rs index e34e8535c..0c4edac1c 100644 --- a/gears/system/account-management/account-management/tests/api_conversions_test.rs +++ b/gears/system/account-management/account-management/tests/api_conversions_test.rs @@ -19,6 +19,8 @@ mod common; use axum::http::{StatusCode, header}; +use toolkit_gts::gts_id; +use toolkit_gts::gts_uri; use tower::ServiceExt; use uuid::Uuid; @@ -127,7 +129,8 @@ async fn request_own_conversion_when_pending_exists_returns_409() { // `type` is `gts://` per toolkit_canonical_errors::Problem; // the `already_exists` category surfaces with the canonical chain. assert_eq!( - body["type"], "gts://gts.cf.core.errors.err.v1~cf.core.err.already_exists.v1~", + body["type"], + gts_uri!("cf.core.errors.err.v1~cf.core.err.already_exists.v1~"), "envelope `type` MUST point at the canonical AlreadyExists GTS id: {body}" ); // `context.resource_type` pins the AM-side resource that collided; @@ -135,7 +138,8 @@ async fn request_own_conversion_when_pending_exists_returns_409() { // the caller can cancel / reject it before retrying without a // separate lookup. assert_eq!( - body["context"]["resource_type"], "gts.cf.core.am.conversion_request.v1~", + body["context"]["resource_type"], + gts_id!("cf.core.am.conversion_request.v1~"), "envelope `context.resource_type` MUST be the conversion-request \ resource (not the tenant resource): {body}" ); diff --git a/gears/system/account-management/account-management/tests/api_status_mapping_test.rs b/gears/system/account-management/account-management/tests/api_status_mapping_test.rs index 4671ce7f6..65dbb391c 100644 --- a/gears/system/account-management/account-management/tests/api_status_mapping_test.rs +++ b/gears/system/account-management/account-management/tests/api_status_mapping_test.rs @@ -51,18 +51,19 @@ mod common; use axum::http::StatusCode; +use toolkit_gts::{gts_id, gts_uri}; use tower::ServiceExt; use uuid::Uuid; use common::*; -const TENANT_GTS: &str = "gts.cf.core.am.tenant.v1~"; -const CONVERSION_REQUEST_GTS: &str = "gts.cf.core.am.conversion_request.v1~"; +const TENANT_GTS: &str = gts_id!("cf.core.am.tenant.v1~"); +const CONVERSION_REQUEST_GTS: &str = gts_id!("cf.core.am.conversion_request.v1~"); const INVALID_ARGUMENT_TYPE: &str = - "gts://gts.cf.core.errors.err.v1~cf.core.err.invalid_argument.v1~"; + gts_uri!("cf.core.errors.err.v1~cf.core.err.invalid_argument.v1~"); const FAILED_PRECONDITION_TYPE: &str = - "gts://gts.cf.core.errors.err.v1~cf.core.err.failed_precondition.v1~"; + gts_uri!("cf.core.errors.err.v1~cf.core.err.failed_precondition.v1~"); #[tokio::test] async fn root_tenant_cannot_delete_envelope() { diff --git a/gears/system/account-management/account-management/tests/api_tenants_test.rs b/gears/system/account-management/account-management/tests/api_tenants_test.rs index 87021dd4a..3f701f83e 100644 --- a/gears/system/account-management/account-management/tests/api_tenants_test.rs +++ b/gears/system/account-management/account-management/tests/api_tenants_test.rs @@ -20,12 +20,13 @@ mod common; use axum::http::{StatusCode, header}; +use toolkit_gts::gts_id; use tower::ServiceExt; use uuid::Uuid; use common::*; -const SAMPLE_TENANT_TYPE: &str = "gts.cf.core.am.tenant_type.v1~cf.core.am.customer.v1~"; +const SAMPLE_TENANT_TYPE: &str = gts_id!("cf.core.am.tenant_type.v1~cf.core.am.customer.v1~"); // ─── POST /tenants ─────────────────────────────────────────────────── diff --git a/gears/system/account-management/account-management/tests/common/mod.rs b/gears/system/account-management/account-management/tests/common/mod.rs index e9a1bb99c..cdbd15418 100644 --- a/gears/system/account-management/account-management/tests/common/mod.rs +++ b/gears/system/account-management/account-management/tests/common/mod.rs @@ -43,6 +43,7 @@ )] use std::sync::Arc; +use toolkit_gts::gts_id; use anyhow::Result; use sea_orm::{ActiveValue, ColumnTrait, Condition, EntityTrait, QueryFilter}; @@ -950,7 +951,8 @@ pub fn inert_resource_checker() /// Mirrors the in-source `domain::user::service_tests::TEST_TENANT_TYPE_ID` /// so the harness lines up byte-for-byte with the canonical AM unit-test /// shape. -pub const HARNESS_TENANT_TYPE_ID: &str = "gts.cf.core.am.tenant_type.v1~cf.core.am.customer.v1~"; +pub const HARNESS_TENANT_TYPE_ID: &str = + gts_id!("cf.core.am.tenant_type.v1~cf.core.am.customer.v1~"); /// The deterministic UUIDv5 derived from [`HARNESS_TENANT_TYPE_ID`]. /// Used as the `tenant_type_uuid` on every tenant row seeded for HTTP @@ -958,14 +960,14 @@ pub const HARNESS_TENANT_TYPE_ID: &str = "gts.cf.core.am.tenant_type.v1~cf.core. /// without a real catalog. #[must_use] pub fn harness_tenant_type_uuid() -> Uuid { - gts::GtsID::new(HARNESS_TENANT_TYPE_ID) + gts::GtsId::try_new(HARNESS_TENANT_TYPE_ID) .expect("HARNESS_TENANT_TYPE_ID is a valid chain") .to_uuid() } /// `MockTypesRegistryClient` pre-seeded with the schemas the AM REST /// surface needs to reach its happy-path branches: the -/// `gts.cf.core.am.user.v1~` user-projection schema (required by +/// AM user-projection schema (required by /// `create_user` per the fail-closed GTS validator) and a minimal /// tenant-type schema chain so `resolve_active_tenant` (which uses /// `tenant_type_uuid` to compute the chained id) does not fail closed. @@ -986,7 +988,7 @@ pub fn types_registry_for_users() -> Arc GtsTypeId { GtsTypeId::new(SCHEMA_A) @@ -72,7 +75,7 @@ fn schema_b() -> GtsTypeId { reason = "test helpers only see hand-crafted valid schema ids" )] fn schema_uuid_for(type_id: &str) -> Uuid { - gts::GtsID::new(type_id) + gts::GtsId::try_new(type_id) .expect("valid GTS id in tests") .to_uuid() } diff --git a/gears/system/account-management/account-management/tests/metadata_integration_pg.rs b/gears/system/account-management/account-management/tests/metadata_integration_pg.rs index 346be495b..2d1174cc4 100644 --- a/gears/system/account-management/account-management/tests/metadata_integration_pg.rs +++ b/gears/system/account-management/account-management/tests/metadata_integration_pg.rs @@ -23,6 +23,7 @@ mod common; use std::sync::Arc; +use toolkit_gts::gts_id; use account_management::domain::metadata::registry::{ InheritancePolicy, MetadataSchemaRegistry, StubMetadataSchemaRegistry, @@ -43,8 +44,10 @@ use uuid::Uuid; use common::pg::bring_up_postgres; use common::*; -const SCHEMA_A: &str = "gts.cf.core.am.tenant_metadata.v1~vendor.app.metadata.feature_flag.v1~"; -const SCHEMA_B: &str = "gts.cf.core.am.tenant_metadata.v1~vendor.app.metadata.org_branding.v1~"; +const SCHEMA_A: &str = + gts_id!("cf.core.am.tenant_metadata.v1~vendor.app.metadata.feature_flag.v1~"); +const SCHEMA_B: &str = + gts_id!("cf.core.am.tenant_metadata.v1~vendor.app.metadata.org_branding.v1~"); fn schema_a() -> GtsTypeId { GtsTypeId::new(SCHEMA_A) diff --git a/gears/system/account-management/plugins/static-idp-plugin/Cargo.toml b/gears/system/account-management/plugins/static-idp-plugin/Cargo.toml index 1397ccda3..b12b6af18 100644 --- a/gears/system/account-management/plugins/static-idp-plugin/Cargo.toml +++ b/gears/system/account-management/plugins/static-idp-plugin/Cargo.toml @@ -57,3 +57,4 @@ tokio = { workspace = true, features = ["rt", "macros"] } # in `domain::client::pagination_tests` builds a sample context per # request. Dev-only so the runtime crate stays free of the dependency. gts = { workspace = true } +toolkit-gts = { workspace = true } diff --git a/gears/system/account-management/plugins/static-idp-plugin/src/domain/client_tests.rs b/gears/system/account-management/plugins/static-idp-plugin/src/domain/client_tests.rs index fccf8d1bd..4c0cfdab7 100644 --- a/gears/system/account-management/plugins/static-idp-plugin/src/domain/client_tests.rs +++ b/gears/system/account-management/plugins/static-idp-plugin/src/domain/client_tests.rs @@ -6,6 +6,7 @@ //! new `CursorV1` key-tuple cursor walk end-to-end. use std::collections::HashSet; +use toolkit_gts::gts_id; use account_management_sdk::{ IdpDeprovisionTenantRequest, IdpDeprovisionUserRequest, IdpListUsersRequest, IdpNewUser, @@ -23,7 +24,7 @@ fn ctx() -> SecurityContext { SecurityContext::anonymous() } -const TENANT_TYPE: &str = "gts.cf.core.am.tenant_type.v1~cf.core.am.customer.v1~"; +const TENANT_TYPE: &str = gts_id!("cf.core.am.tenant_type.v1~cf.core.am.customer.v1~"); fn tenant_type() -> gts::GtsTypeId { gts::GtsTypeId::new(TENANT_TYPE) diff --git a/gears/system/api-gateway/Cargo.toml b/gears/system/api-gateway/Cargo.toml index e6ea57347..31c761e34 100644 --- a/gears/system/api-gateway/Cargo.toml +++ b/gears/system/api-gateway/Cargo.toml @@ -24,6 +24,7 @@ toolkit-http = { workspace = true } toolkit-security = { workspace = true } toolkit-canonical-errors = { workspace = true, features = ["axum"] } authn-resolver-sdk = { package = "cf-gears-authn-resolver-sdk", version = "0.3.18", path = "../authn-resolver/authn-resolver-sdk" } +toolkit-gts = { workspace = true } toolkit-macros = { workspace = true } inventory = { workspace = true } anyhow = { workspace = true } diff --git a/gears/system/api-gateway/src/middleware/errors.rs b/gears/system/api-gateway/src/middleware/errors.rs index ac48055a5..5f4b98023 100644 --- a/gears/system/api-gateway/src/middleware/errors.rs +++ b/gears/system/api-gateway/src/middleware/errors.rs @@ -3,7 +3,7 @@ use toolkit_canonical_errors::resource_error; /// Errors attributable to a registered API gateway route /// (scope / license / RBAC). -#[resource_error("gts.cf.core.api_gateway.route.v1~")] +#[resource_error(gts_id!("cf.core.api_gateway.route.v1~"))] pub struct ApiGatewayRouteError; /// Umbrella scope for request-pipeline errors that don't target a @@ -12,5 +12,5 @@ pub struct ApiGatewayRouteError; /// and `deadline_exceeded` are only available on `#[resource_error]` /// scopes — there are no top-level `CanonicalError::*` constructors for /// those categories. -#[resource_error("gts.cf.core.api_gateway.gateway.v1~")] +#[resource_error(gts_id!("cf.core.api_gateway.gateway.v1~"))] pub struct ApiGatewayGatewayError; diff --git a/gears/system/api-gateway/src/middleware/license_validation.rs b/gears/system/api-gateway/src/middleware/license_validation.rs index 3d483f181..99f776df4 100644 --- a/gears/system/api-gateway/src/middleware/license_validation.rs +++ b/gears/system/api-gateway/src/middleware/license_validation.rs @@ -5,11 +5,12 @@ use dashmap::DashMap; use http::Method; use std::sync::Arc; use toolkit::api::OperationSpec; +use toolkit_gts::gts_id; use crate::middleware::common; use crate::middleware::errors::ApiGatewayRouteError; -const BASE_FEATURE: &str = "gts.cf.core.lic.feat.v1~cf.core.global.base.v1"; +const BASE_FEATURE: &str = gts_id!("cf.core.lic.feat.v1~cf.core.global.base.v1"); type LicenseKey = (Method, String); diff --git a/gears/system/api-gateway/src/middleware/scope_enforcement.rs b/gears/system/api-gateway/src/middleware/scope_enforcement.rs index ef9bd92f6..0cd6f8247 100644 --- a/gears/system/api-gateway/src/middleware/scope_enforcement.rs +++ b/gears/system/api-gateway/src/middleware/scope_enforcement.rs @@ -252,6 +252,7 @@ mod tests { use super::*; use crate::config::RoutePolicyRule; use toolkit_canonical_errors::Problem; + use toolkit_gts::gts_uri; fn build_config(enabled: bool, routes: Vec<(&str, Vec<&str>)>) -> RoutePoliciesConfig { build_config_with_methods( @@ -334,7 +335,7 @@ mod tests { assert_eq!(problem.status, 403); assert_eq!( problem.problem_type, - "gts://gts.cf.core.errors.err.v1~cf.core.err.permission_denied.v1~" + gts_uri!("cf.core.errors.err.v1~cf.core.err.permission_denied.v1~") ); assert_eq!(problem.context["reason"], "INSUFFICIENT_SCOPES"); } @@ -353,7 +354,7 @@ mod tests { assert_eq!(problem.status, 403); assert_eq!( problem.problem_type, - "gts://gts.cf.core.errors.err.v1~cf.core.err.permission_denied.v1~" + gts_uri!("cf.core.errors.err.v1~cf.core.err.permission_denied.v1~") ); } diff --git a/gears/system/api-gateway/tests/auth_middleware.rs b/gears/system/api-gateway/tests/auth_middleware.rs index ff25d482b..9f09b366a 100644 --- a/gears/system/api-gateway/tests/auth_middleware.rs +++ b/gears/system/api-gateway/tests/auth_middleware.rs @@ -23,23 +23,27 @@ use serde_json::json; use std::sync::Arc; use toolkit::{ ClientHub, Gear, - api::{OperationBuilder, operation_builder::LicenseFeature}, + api::{ + OperationBuilder, + operation_builder::{CORE_GLOBAL_BASE_LICENSE_FEATURE, LicenseFeature}, + }, config::ConfigProvider, context::GearCtx, contracts::{ApiGatewayCapability, OpenApiRegistry, RestApiCapability}, }; use toolkit_canonical_errors::Problem; +use toolkit_gts::gts_uri; use toolkit_security::SecurityContext; use tower::ServiceExt; use uuid::Uuid; const UNAUTHENTICATED_TYPE: &str = - "gts://gts.cf.core.errors.err.v1~cf.core.err.unauthenticated.v1~"; + gts_uri!("cf.core.errors.err.v1~cf.core.err.unauthenticated.v1~"); const SERVICE_UNAVAILABLE_TYPE: &str = - "gts://gts.cf.core.errors.err.v1~cf.core.err.service_unavailable.v1~"; -const INTERNAL_TYPE: &str = "gts://gts.cf.core.errors.err.v1~cf.core.err.internal.v1~"; + gts_uri!("cf.core.errors.err.v1~cf.core.err.service_unavailable.v1~"); +const INTERNAL_TYPE: &str = gts_uri!("cf.core.errors.err.v1~cf.core.err.internal.v1~"); const PERMISSION_DENIED_TYPE: &str = - "gts://gts.cf.core.errors.err.v1~cf.core.err.permission_denied.v1~"; + gts_uri!("cf.core.errors.err.v1~cf.core.err.permission_denied.v1~"); const PROBLEM_JSON: &str = "application/problem+json"; async fn problem_from(response: Response) -> Problem { @@ -144,7 +148,7 @@ struct License; impl AsRef for License { fn as_ref(&self) -> &'static str { - "gts.cf.core.lic.feat.v1~cf.core.global.base.v1" + CORE_GLOBAL_BASE_LICENSE_FEATURE } } diff --git a/gears/system/api-gateway/tests/auth_middleware.rs.orig b/gears/system/api-gateway/tests/auth_middleware.rs.orig index 19ae84c61..4860bd6d6 100644 --- a/gears/system/api-gateway/tests/auth_middleware.rs.orig +++ b/gears/system/api-gateway/tests/auth_middleware.rs.orig @@ -34,10 +34,10 @@ use tower::ServiceExt; use uuid::Uuid; const UNAUTHENTICATED_TYPE: &str = - "gts://gts.cf.core.errors.err.v1~cf.core.err.unauthenticated.v1~"; + concat!("gts://", "gts", ".", "cf.core.errors.err.v1~cf.core.err.unauthenticated.v1~"); const SERVICE_UNAVAILABLE_TYPE: &str = - "gts://gts.cf.core.errors.err.v1~cf.core.err.service_unavailable.v1~"; -const INTERNAL_TYPE: &str = "gts://gts.cf.core.errors.err.v1~cf.core.err.internal.v1~"; + concat!("gts://", "gts", ".", "cf.core.errors.err.v1~cf.core.err.service_unavailable.v1~"); +const INTERNAL_TYPE: &str = concat!("gts://", "gts", ".", "cf.core.errors.err.v1~cf.core.err.internal.v1~"); const PROBLEM_JSON: &str = "application/problem+json"; async fn problem_from(response: Response) -> Problem { diff --git a/gears/system/api-gateway/tests/license_middleware.rs b/gears/system/api-gateway/tests/license_middleware.rs index f4c057b32..045645c49 100644 --- a/gears/system/api-gateway/tests/license_middleware.rs +++ b/gears/system/api-gateway/tests/license_middleware.rs @@ -13,17 +13,18 @@ use std::sync::Arc; use toolkit::{ ClientHub, Gear, api::OperationBuilder, - api::operation_builder::LicenseFeature, + api::operation_builder::{CORE_GLOBAL_BASE_LICENSE_FEATURE, LicenseFeature}, config::ConfigProvider, context::GearCtx, contracts::{ApiGatewayCapability, OpenApiRegistry, RestApiCapability}, }; use toolkit_canonical_errors::Problem; +use toolkit_gts::gts_uri; use tower::ServiceExt; use uuid::Uuid; const PERMISSION_DENIED_TYPE: &str = - "gts://gts.cf.core.errors.err.v1~cf.core.err.permission_denied.v1~"; + gts_uri!("cf.core.errors.err.v1~cf.core.err.permission_denied.v1~"); const PROBLEM_JSON: &str = "application/problem+json"; struct TestConfigProvider { @@ -85,7 +86,7 @@ struct BaseFeature; impl AsRef for BaseFeature { fn as_ref(&self) -> &'static str { - "gts.cf.core.lic.feat.v1~cf.core.global.base.v1" + CORE_GLOBAL_BASE_LICENSE_FEATURE } } diff --git a/gears/system/api-gateway/tests/license_middleware.rs.orig b/gears/system/api-gateway/tests/license_middleware.rs.orig index e8a2a62e8..2ad586ba9 100644 --- a/gears/system/api-gateway/tests/license_middleware.rs.orig +++ b/gears/system/api-gateway/tests/license_middleware.rs.orig @@ -23,7 +23,7 @@ use tower::ServiceExt; use uuid::Uuid; const PERMISSION_DENIED_TYPE: &str = - "gts://gts.cf.core.errors.err.v1~cf.core.err.permission_denied.v1~"; + concat!("gts://", "gts", ".", "cf.core.errors.err.v1~cf.core.err.permission_denied.v1~"); const PROBLEM_JSON: &str = "application/problem+json"; struct TestConfigProvider { diff --git a/gears/system/api-gateway/tests/mime_validation_integration.rs b/gears/system/api-gateway/tests/mime_validation_integration.rs index 471e93f06..4a641b824 100644 --- a/gears/system/api-gateway/tests/mime_validation_integration.rs +++ b/gears/system/api-gateway/tests/mime_validation_integration.rs @@ -16,6 +16,7 @@ use http::Method; use serde_json::json; use toolkit::api::OperationSpec; use toolkit_canonical_errors::Problem; +use toolkit_gts::gts_uri; use tower::ServiceExt; // for oneshot use api_gateway::middleware::mime_validation::{ @@ -24,7 +25,7 @@ use api_gateway::middleware::mime_validation::{ use toolkit::api::operation_builder::VendorExtensions; const INVALID_ARGUMENT_TYPE: &str = - "gts://gts.cf.core.errors.err.v1~cf.core.err.invalid_argument.v1~"; + gts_uri!("cf.core.errors.err.v1~cf.core.err.invalid_argument.v1~"); const PROBLEM_JSON: &str = "application/problem+json"; /// Helper to extract Problem from response diff --git a/gears/system/api-gateway/tests/rate_limit_tests.rs b/gears/system/api-gateway/tests/rate_limit_tests.rs index c16fa3303..dd4b53a4f 100644 --- a/gears/system/api-gateway/tests/rate_limit_tests.rs +++ b/gears/system/api-gateway/tests/rate_limit_tests.rs @@ -21,14 +21,15 @@ use toolkit::{ contracts::{ApiGatewayCapability, OpenApiRegistry}, }; use toolkit_canonical_errors::Problem; +use toolkit_gts::gts_uri; use tower::ServiceExt; use utoipa::ToSchema; use uuid::Uuid; const RESOURCE_EXHAUSTED_TYPE: &str = - "gts://gts.cf.core.errors.err.v1~cf.core.err.resource_exhausted.v1~"; + gts_uri!("cf.core.errors.err.v1~cf.core.err.resource_exhausted.v1~"); const SERVICE_UNAVAILABLE_TYPE: &str = - "gts://gts.cf.core.errors.err.v1~cf.core.err.service_unavailable.v1~"; + gts_uri!("cf.core.errors.err.v1~cf.core.err.service_unavailable.v1~"); const PROBLEM_JSON: &str = "application/problem+json"; /// Helper to create a test `GearCtx` diff --git a/gears/system/authn-resolver/authn-resolver-sdk/src/gts.rs b/gears/system/authn-resolver/authn-resolver-sdk/src/gts.rs index 621269f2c..1750d6490 100644 --- a/gears/system/authn-resolver/authn-resolver-sdk/src/gts.rs +++ b/gears/system/authn-resolver/authn-resolver-sdk/src/gts.rs @@ -42,7 +42,7 @@ use toolkit_gts::gts_type_schema; #[gts_type_schema( dir_path = "schemas", base = PluginV1, - type_id = "gts.cf.toolkit.plugins.plugin.v1~cf.core.authn_resolver.plugin.v1~", + type_id = gts_id!("cf.toolkit.plugins.plugin.v1~cf.core.authn_resolver.plugin.v1~"), description = "AuthN Resolver plugin specification", properties = "", )] diff --git a/gears/system/authn-resolver/plugins/oidc-authn-plugin/Cargo.toml b/gears/system/authn-resolver/plugins/oidc-authn-plugin/Cargo.toml index ac1c813a6..1b6fb25a4 100644 --- a/gears/system/authn-resolver/plugins/oidc-authn-plugin/Cargo.toml +++ b/gears/system/authn-resolver/plugins/oidc-authn-plugin/Cargo.toml @@ -27,6 +27,7 @@ types-registry-sdk = { # ToolKit dependencies toolkit.workspace = true +toolkit-gts = { workspace = true } toolkit-macros.workspace = true toolkit-security.workspace = true inventory.workspace = true diff --git a/gears/system/authn-resolver/plugins/oidc-authn-plugin/benches/s2s_exchange.rs b/gears/system/authn-resolver/plugins/oidc-authn-plugin/benches/s2s_exchange.rs index cf8ec84a7..3e9328ff7 100644 --- a/gears/system/authn-resolver/plugins/oidc-authn-plugin/benches/s2s_exchange.rs +++ b/gears/system/authn-resolver/plugins/oidc-authn-plugin/benches/s2s_exchange.rs @@ -10,6 +10,7 @@ use std::sync::Arc; use std::time::{Duration, Instant}; +use toolkit_gts::gts_id; use authn_resolver_sdk::{AuthNResolverPluginClient, ClientCredentialsRequest}; use criterion::{Criterion, criterion_group, criterion_main}; @@ -38,7 +39,7 @@ const REALM_PATH: &str = "/realms/platform"; const DISCOVERY_PATH: &str = "/realms/platform/.well-known/openid-configuration"; const JWKS_PATH: &str = "/realms/platform/protocol/openid-connect/certs"; const TOKEN_PATH: &str = "/realms/platform/protocol/openid-connect/token"; -const BENCH_S2S_DEFAULT_SUBJECT_TYPE: &str = "gts.cf.core.security.subject_user.v1~"; +const BENCH_S2S_DEFAULT_SUBJECT_TYPE: &str = gts_id!("cf.core.security.subject_user.v1~"); struct MockOidcServer { server: MockServer, diff --git a/gears/system/authn-resolver/plugins/oidc-authn-plugin/src/config.rs b/gears/system/authn-resolver/plugins/oidc-authn-plugin/src/config.rs index 4e23a07eb..65f91717e 100644 --- a/gears/system/authn-resolver/plugins/oidc-authn-plugin/src/config.rs +++ b/gears/system/authn-resolver/plugins/oidc-authn-plugin/src/config.rs @@ -11,6 +11,9 @@ use serde::Deserialize; use crate::domain::claim_mapper::{ClaimMapperConfig, ClaimMapperOptions}; use crate::infra::url_policy::UrlSecurityPolicy; +#[cfg(test)] +use toolkit_gts::gts_id; + /// Stable plugin instance suffix used by `AuthN` resolver plugin selection. pub const INSTANCE_SUFFIX: &str = "cf.builtin.oidc_authn_resolver.plugin.v1"; @@ -1113,6 +1116,14 @@ fn parse_duration_millis(input: &str) -> Result { .map_err(|_| anyhow!("duration too large in milliseconds: {input:?}")) } +#[cfg(test)] +fn expand_test_gts(json: &str) -> String { + json.replace("{subject_user_type}", SUBJECT_USER_TYPE) +} + +#[cfg(test)] +const SUBJECT_USER_TYPE: &str = gts_id!("cf.core.security.subject_user.v1~"); + #[cfg(test)] mod tests { use super::*; @@ -1509,11 +1520,11 @@ mod gear_input_tests { "s2s_oauth": { "discovery_url": "https://oidc/realms/platform", "token_cache": { "ttl": "600s", "max_entries": 120 }, - "default_subject_type": "gts.cf.core.security.subject_user.v1~" + "default_subject_type": "{subject_user_type}" } }"#; - let config: OidcAuthNGearConfig = - serde_json::from_str(json).expect("documented config should deserialize"); + let config: OidcAuthNGearConfig = serde_json::from_str(&expand_test_gts(json)) + .expect("documented config should deserialize"); let resolved = config .resolve() .expect("documented config should resolve to runtime config"); @@ -1558,10 +1569,7 @@ mod gear_input_tests { resolved.plugin.s2s.discovery_url.as_str(), "https://oidc/realms/platform" ); - assert_eq!( - resolved.plugin.s2s_default_subject_type, - "gts.cf.core.security.subject_user.v1~" - ); + assert_eq!(resolved.plugin.s2s_default_subject_type, SUBJECT_USER_TYPE); assert_eq!(resolved.plugin.s2s.token_cache_ttl_secs, 600); assert_eq!(resolved.plugin.s2s.token_cache_max_entries, 120); assert_eq!(resolved.request_timeout, Duration::from_secs(5)); @@ -1589,11 +1597,11 @@ mod gear_input_tests { }, "s2s_oauth": { "discovery_url": "https://obo.example.com", - "default_subject_type": "gts.cf.core.security.subject_user.v1~" + "default_subject_type": "{subject_user_type}" } }"#; - let config: OidcAuthNGearConfig = - serde_json::from_str(json).expect("per-issuer override config should deserialize"); + let config: OidcAuthNGearConfig = serde_json::from_str(&expand_test_gts(json)) + .expect("per-issuer override config should deserialize"); let resolved = config .resolve() .expect("per-issuer override config should resolve"); @@ -1629,7 +1637,7 @@ mod gear_input_tests { "discovery_url": "https://oidc/realms/platform" } }"#; - let error = serde_json::from_str::(json) + let error = serde_json::from_str::(&expand_test_gts(json)) .expect_err("missing S2S default subject type should fail deserialization"); assert!( @@ -1652,8 +1660,8 @@ mod gear_input_tests { "default_subject_type": " " } }"#; - let config: OidcAuthNGearConfig = - serde_json::from_str(json).expect("blank string should deserialize before validation"); + let config: OidcAuthNGearConfig = serde_json::from_str(&expand_test_gts(json)) + .expect("blank string should deserialize before validation"); let error = config .resolve() .expect_err("blank S2S default subject type should fail resolution"); @@ -1676,11 +1684,11 @@ mod gear_input_tests { "http_client": { "custom_ca_certificate_paths": [" "] }, "s2s_oauth": { "discovery_url": "https://oidc/realms/platform", - "default_subject_type": "gts.cf.core.security.subject_user.v1~" + "default_subject_type": "{subject_user_type}" } }"#; - let config: OidcAuthNGearConfig = - serde_json::from_str(json).expect("blank CA path should deserialize before validation"); + let config: OidcAuthNGearConfig = serde_json::from_str(&expand_test_gts(json)) + .expect("blank CA path should deserialize before validation"); let error = config .resolve() .expect_err("blank CA path should fail resolution"); @@ -1703,11 +1711,11 @@ mod gear_input_tests { "http_client": { "request_timeout": "0s" }, "s2s_oauth": { "discovery_url": "https://oidc/realms/platform", - "default_subject_type": "gts.cf.core.security.subject_user.v1~" + "default_subject_type": "{subject_user_type}" } }"#; - let config: OidcAuthNGearConfig = - serde_json::from_str(json).expect("zero request timeout should deserialize"); + let config: OidcAuthNGearConfig = serde_json::from_str(&expand_test_gts(json)) + .expect("zero request timeout should deserialize"); let error = config .resolve() .expect_err("zero request timeout should fail resolution"); @@ -1729,11 +1737,11 @@ mod gear_input_tests { }, "s2s_oauth": { "discovery_url": " ", - "default_subject_type": "gts.cf.core.security.subject_user.v1~" + "default_subject_type": "{subject_user_type}" } }"#; - let config: OidcAuthNGearConfig = - serde_json::from_str(json).expect("blank string should deserialize before validation"); + let config: OidcAuthNGearConfig = serde_json::from_str(&expand_test_gts(json)) + .expect("blank string should deserialize before validation"); let error = config .resolve() .expect_err("blank S2S discovery URL should fail resolution"); @@ -1755,10 +1763,10 @@ mod gear_input_tests { }, "s2s_oauth": { "discovery_url": "http://oidc/realms/platform", - "default_subject_type": "gts.cf.core.security.subject_user.v1~" + "default_subject_type": "{subject_user_type}" } }"#; - let error = serde_json::from_str::(json) + let error = serde_json::from_str::(&expand_test_gts(json)) .expect("config JSON should parse") .resolve() .expect_err("HTTP S2S discovery URL should fail under default URL policy"); @@ -1778,11 +1786,11 @@ mod gear_input_tests { }, "s2s_oauth": { "discovery_url": " https://oidc/realms/platform ", - "default_subject_type": "gts.cf.core.security.subject_user.v1~" + "default_subject_type": "{subject_user_type}" } }"#; - let config: OidcAuthNGearConfig = - serde_json::from_str(json).expect("padded discovery URL should deserialize"); + let config: OidcAuthNGearConfig = serde_json::from_str(&expand_test_gts(json)) + .expect("padded discovery URL should deserialize"); let resolved = config .resolve() .expect("padded discovery URL should resolve"); @@ -1802,10 +1810,10 @@ mod gear_input_tests { }, "s2s_oauth": { "discovery_url": "https://oidc/realms/platform", - "default_subject_type": "gts.cf.core.security.subject_user.v1~" + "default_subject_type": "{subject_user_type}" } }"#; - let config: OidcAuthNGearConfig = serde_json::from_str(json) + let config: OidcAuthNGearConfig = serde_json::from_str(&expand_test_gts(json)) .expect("blank subject tenant ID should deserialize before validation"); let error = config .resolve() @@ -1829,11 +1837,11 @@ mod gear_input_tests { "s2s_oauth": { "discovery_url": "https://oidc/realms/platform", "claim_mapping": { "subject_tenant_id": " s2s_tenant_id " }, - "default_subject_type": "gts.cf.core.security.subject_user.v1~" + "default_subject_type": "{subject_user_type}" } }"#; - let config: OidcAuthNGearConfig = - serde_json::from_str(json).expect("padded subject tenant IDs should deserialize"); + let config: OidcAuthNGearConfig = serde_json::from_str(&expand_test_gts(json)) + .expect("padded subject tenant IDs should deserialize"); let resolved = config .resolve() .expect("padded subject tenant IDs should resolve"); @@ -1856,11 +1864,11 @@ mod gear_input_tests { }, "s2s_oauth": { "discovery_url": "https://oidc/realms/platform", - "default_subject_type": "gts.cf.core.security.subject_user.v1~" + "default_subject_type": "{subject_user_type}" } }"#; - let config: OidcAuthNGearConfig = - serde_json::from_str(json).expect("optional audience config should deserialize"); + let config: OidcAuthNGearConfig = serde_json::from_str(&expand_test_gts(json)) + .expect("optional audience config should deserialize"); let resolved = config .resolve() .expect("optional audience config should resolve"); @@ -1883,11 +1891,11 @@ mod gear_input_tests { }, "s2s_oauth": { "discovery_url": "https://oidc/realms/platform", - "default_subject_type": "gts.cf.core.security.subject_user.v1~" + "default_subject_type": "{subject_user_type}" } }"#; - let config: OidcAuthNGearConfig = - serde_json::from_str(json).expect("algorithm subset config should deserialize"); + let config: OidcAuthNGearConfig = serde_json::from_str(&expand_test_gts(json)) + .expect("algorithm subset config should deserialize"); let resolved = config .resolve() .expect("algorithm subset config should resolve"); @@ -1909,11 +1917,11 @@ mod gear_input_tests { }, "s2s_oauth": { "discovery_url": "https://oidc/realms/platform", - "default_subject_type": "gts.cf.core.security.subject_user.v1~" + "default_subject_type": "{subject_user_type}" } }"#; - let config: OidcAuthNGearConfig = - serde_json::from_str(json).expect("unsupported algorithm config should deserialize"); + let config: OidcAuthNGearConfig = serde_json::from_str(&expand_test_gts(json)) + .expect("unsupported algorithm config should deserialize"); let error = config .resolve() .expect_err("unsupported algorithm should fail resolution"); @@ -1936,11 +1944,11 @@ mod gear_input_tests { }, "s2s_oauth": { "discovery_url": "https://oidc/realms/platform", - "default_subject_type": "gts.cf.core.security.subject_user.v1~" + "default_subject_type": "{subject_user_type}" } }"#; - let config: OidcAuthNGearConfig = - serde_json::from_str(json).expect("clock skew config should deserialize"); + let config: OidcAuthNGearConfig = serde_json::from_str(&expand_test_gts(json)) + .expect("clock skew config should deserialize"); let error = config .resolve() .expect_err("clock skew over 300s should fail resolution"); @@ -1963,11 +1971,11 @@ mod gear_input_tests { "jwks_cache": { "ttl": "10m", "stale_ttl": "5m" }, "s2s_oauth": { "discovery_url": "https://oidc/realms/platform", - "default_subject_type": "gts.cf.core.security.subject_user.v1~" + "default_subject_type": "{subject_user_type}" } }"#; - let config: OidcAuthNGearConfig = - serde_json::from_str(json).expect("stale ttl config should deserialize"); + let config: OidcAuthNGearConfig = serde_json::from_str(&expand_test_gts(json)) + .expect("stale ttl config should deserialize"); let error = config .resolve() .expect_err("stale ttl shorter than fresh ttl should fail resolution"); @@ -1992,12 +2000,12 @@ mod gear_input_tests { "circuit_breaker": {{ "reset_timeout": "{reset_timeout}" }}, "s2s_oauth": {{ "discovery_url": "https://oidc/realms/platform", - "default_subject_type": "gts.cf.core.security.subject_user.v1~" + "default_subject_type": "{{subject_user_type}}" }} }}"# ); - let config: OidcAuthNGearConfig = - serde_json::from_str(&json).expect("reset timeout config should deserialize"); + let config: OidcAuthNGearConfig = serde_json::from_str(&expand_test_gts(&json)) + .expect("reset timeout config should deserialize"); let error = config .resolve() .expect_err("zero reset timeout should fail resolution"); @@ -2021,11 +2029,11 @@ mod gear_input_tests { "jwks_cache": { "ttl": "10m", "stale_ttl": "0s" }, "s2s_oauth": { "discovery_url": "https://oidc/realms/platform", - "default_subject_type": "gts.cf.core.security.subject_user.v1~" + "default_subject_type": "{subject_user_type}" } }"#; - let config: OidcAuthNGearConfig = - serde_json::from_str(json).expect("zero stale ttl config should deserialize"); + let config: OidcAuthNGearConfig = serde_json::from_str(&expand_test_gts(json)) + .expect("zero stale ttl config should deserialize"); let resolved = config .resolve() .expect("zero stale ttl should disable stale fallback"); diff --git a/gears/system/authn-resolver/plugins/oidc-authn-plugin/src/domain/authenticate_tests.rs b/gears/system/authn-resolver/plugins/oidc-authn-plugin/src/domain/authenticate_tests.rs index d17b3257b..e19ec1869 100644 --- a/gears/system/authn-resolver/plugins/oidc-authn-plugin/src/domain/authenticate_tests.rs +++ b/gears/system/authn-resolver/plugins/oidc-authn-plugin/src/domain/authenticate_tests.rs @@ -1,4 +1,5 @@ use std::sync::Arc; +use toolkit_gts::gts_id; use async_trait::async_trait; use authn_resolver_sdk::ClientCredentialsRequest; @@ -14,7 +15,7 @@ use crate::test_support::test_fixtures::{ TEST_ISSUER, TEST_KID, future_exp, sign_jwt, test_jwk_json, }; -const TEST_S2S_DEFAULT_SUBJECT_TYPE: &str = "gts.cf.core.security.subject_user.v1~"; +const TEST_S2S_DEFAULT_SUBJECT_TYPE: &str = gts_id!("cf.core.security.subject_user.v1~"); fn test_issuer_trust() -> IssuerTrustConfig { IssuerTrustConfig::from_exact_issuers(vec![TEST_ISSUER.to_owned()]) diff --git a/gears/system/authn-resolver/plugins/oidc-authn-plugin/tests/common/mod.rs b/gears/system/authn-resolver/plugins/oidc-authn-plugin/tests/common/mod.rs index 7a76a55f9..69aa21ec6 100644 --- a/gears/system/authn-resolver/plugins/oidc-authn-plugin/tests/common/mod.rs +++ b/gears/system/authn-resolver/plugins/oidc-authn-plugin/tests/common/mod.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use std::sync::OnceLock; use std::sync::{Arc, Mutex}; +use toolkit_gts::gts_id; use httpmock::prelude::{GET, HttpMockRequest, HttpMockResponse, MockServer, POST}; use jsonwebtoken::jwk::{Jwk, JwkSet, PublicKeyUse}; @@ -59,7 +60,7 @@ pub const TEST_S2S_CLIENT_ID: &str = "svc-test"; /// Valid test client secret accepted by the mock token endpoint. pub const TEST_S2S_CLIENT_SECRET: &str = "test-secret-value"; /// Default S2S subject type used by integration-test plugin fixtures. -pub const TEST_S2S_DEFAULT_SUBJECT_TYPE: &str = "gts.cf.core.security.subject_user.v1~"; +pub const TEST_S2S_DEFAULT_SUBJECT_TYPE: &str = gts_id!("cf.core.security.subject_user.v1~"); type RequestCounters = Arc>>; diff --git a/gears/system/authn-resolver/plugins/static-authn-plugin/src/config.rs b/gears/system/authn-resolver/plugins/static-authn-plugin/src/config.rs index b6bf0e01f..fd8385449 100644 --- a/gears/system/authn-resolver/plugins/static-authn-plugin/src/config.rs +++ b/gears/system/authn-resolver/plugins/static-authn-plugin/src/config.rs @@ -67,7 +67,7 @@ pub struct IdentityConfig { pub token_scopes: Vec, /// Subject type — opaque metadata passed through to PDP via `EvaluationRequest.Subject`. - /// Recommended format: GTS type identifier (e.g. `"gts.cf.core.security.subject_user.v1~"`). + /// Recommended format: GTS type identifier produced via `gts_id!(...)`. /// The platform does not interpret this value; PDP policies may use it for role/permission mapping. pub subject_type: Option, } diff --git a/gears/system/authz-resolver/authz-resolver-sdk/src/gts.rs b/gears/system/authz-resolver/authz-resolver-sdk/src/gts.rs index f380bbc03..609608a4d 100644 --- a/gears/system/authz-resolver/authz-resolver-sdk/src/gts.rs +++ b/gears/system/authz-resolver/authz-resolver-sdk/src/gts.rs @@ -14,7 +14,7 @@ use toolkit_gts::gts_type_schema; #[gts_type_schema( dir_path = "schemas", base = PluginV1, - type_id = "gts.cf.toolkit.plugins.plugin.v1~cf.core.authz_resolver.plugin.v1~", + type_id = gts_id!("cf.toolkit.plugins.plugin.v1~cf.core.authz_resolver.plugin.v1~"), description = "AuthZ Resolver plugin specification", properties = "", )] diff --git a/gears/system/authz-resolver/authz-resolver-sdk/src/lib.rs b/gears/system/authz-resolver/authz-resolver-sdk/src/lib.rs index d2681e41f..8d1ace518 100644 --- a/gears/system/authz-resolver/authz-resolver-sdk/src/lib.rs +++ b/gears/system/authz-resolver/authz-resolver-sdk/src/lib.rs @@ -19,7 +19,7 @@ //! }; //! //! const USER: ResourceType = ResourceType::from_static( -//! "gts.cf.core.users.user.v1~", +//! gts_id!("cf.core.users.user.v1~"), //! &["owner_tenant_id", "id"], //! ); //! diff --git a/gears/system/authz-resolver/authz-resolver-sdk/src/pep/enforcer.rs b/gears/system/authz-resolver/authz-resolver-sdk/src/pep/enforcer.rs index 3bdc3352a..88c9e2fc2 100644 --- a/gears/system/authz-resolver/authz-resolver-sdk/src/pep/enforcer.rs +++ b/gears/system/authz-resolver/authz-resolver-sdk/src/pep/enforcer.rs @@ -205,7 +205,7 @@ impl ResourceType { } } - /// Dotted resource type name (e.g. `"gts.cf.core.users.user.v1~"`). + /// Dotted resource type name (for example, a `gts_id!(...)` value). #[must_use] pub fn name(&self) -> &str { &self.name @@ -232,7 +232,7 @@ impl ResourceType { /// use toolkit_security::pep_properties; /// /// const USER: ResourceType = ResourceType::from_static( -/// "gts.cf.core.users.user.v1~", +/// gts_id!("cf.core.users.user.v1~"), /// &[pep_properties::OWNER_TENANT_ID, pep_properties::RESOURCE_ID], /// ); /// diff --git a/gears/system/authz-resolver/authz-resolver-sdk/src/pep/enforcer_tests.rs b/gears/system/authz-resolver/authz-resolver-sdk/src/pep/enforcer_tests.rs index 0b6ef7172..e4a56c8bf 100644 --- a/gears/system/authz-resolver/authz-resolver-sdk/src/pep/enforcer_tests.rs +++ b/gears/system/authz-resolver/authz-resolver-sdk/src/pep/enforcer_tests.rs @@ -4,6 +4,7 @@ use async_trait::async_trait; use super::*; use crate::constraints::{Constraint, InPredicate, Predicate}; use crate::models::{EvaluationResponse, EvaluationResponseContext}; +use toolkit_gts::gts_id; use toolkit_security::pep_properties; fn uuid(s: &str) -> Uuid { @@ -128,7 +129,7 @@ fn test_ctx() -> SecurityContext { } const TEST_RESOURCE: ResourceType = ResourceType::from_static( - "gts.cf.core.users.user.v1~", + gts_id!("cf.core.users.user.v1~"), &[pep_properties::OWNER_TENANT_ID, pep_properties::RESOURCE_ID], ); @@ -144,7 +145,10 @@ fn build_request_populates_fields() { let ctx = test_ctx(); let req = e.build_request(&ctx, &TEST_RESOURCE, "get", Some(uuid(RESOURCE)), true); - assert_eq!(req.resource.resource_type, "gts.cf.core.users.user.v1~"); + assert_eq!( + req.resource.resource_type, + gts_id!("cf.core.users.user.v1~") + ); assert_eq!(req.action.name, "get"); assert_eq!(req.resource.id, Some(uuid(RESOURCE))); assert!(req.context.require_constraints); @@ -508,7 +512,7 @@ async fn access_scope_with_resource_properties() { #[test] fn builds_request_with_all_fields() { const USERS_RESOURCE: ResourceType = ResourceType::from_static( - "gts.cf.core.users.user.v1~", + gts_id!("cf.core.users.user.v1~"), &[pep_properties::OWNER_TENANT_ID], ); @@ -550,7 +554,10 @@ fn builds_request_with_all_fields() { ); assert_eq!(request.subject.subject_type.as_deref(), Some("user")); assert_eq!(request.action.name, "get"); - assert_eq!(request.resource.resource_type, "gts.cf.core.users.user.v1~"); + assert_eq!( + request.resource.resource_type, + gts_id!("cf.core.users.user.v1~") + ); assert_eq!(request.resource.id, Some(resource_id)); assert!(request.context.require_constraints); assert_eq!( diff --git a/gears/system/authz-resolver/plugins/static-authz-plugin/Cargo.toml b/gears/system/authz-resolver/plugins/static-authz-plugin/Cargo.toml index 4c017a149..d92a4d101 100644 --- a/gears/system/authz-resolver/plugins/static-authz-plugin/Cargo.toml +++ b/gears/system/authz-resolver/plugins/static-authz-plugin/Cargo.toml @@ -24,6 +24,7 @@ types-registry-sdk = { package = "cf-gears-types-registry-sdk", version = "0.3.0 # ToolKit dependencies toolkit = { workspace = true } +toolkit-gts = { workspace = true } toolkit-macros = { workspace = true } toolkit-security = { workspace = true } diff --git a/gears/system/authz-resolver/plugins/static-authz-plugin/src/domain/service_tests.rs b/gears/system/authz-resolver/plugins/static-authz-plugin/src/domain/service_tests.rs index eca20d830..bef672d4f 100644 --- a/gears/system/authz-resolver/plugins/static-authz-plugin/src/domain/service_tests.rs +++ b/gears/system/authz-resolver/plugins/static-authz-plugin/src/domain/service_tests.rs @@ -3,6 +3,7 @@ use super::*; use authz_resolver_sdk::pep::IntoPropertyValue; use authz_resolver_sdk::{Action, EvaluationRequestContext, Resource, Subject, TenantContext}; use std::collections::HashMap; +use toolkit_gts::gts_id; fn make_request(require_constraints: bool, tenant_id: Option) -> EvaluationRequest { let mut subject_properties = HashMap::new(); @@ -21,7 +22,7 @@ fn make_request(require_constraints: bool, tenant_id: Option) -> Evaluatio name: "list".to_owned(), }, resource: Resource { - resource_type: "gts.cf.core.users.user.v1~".to_owned(), + resource_type: gts_id!("cf.core.users.user.v1~").to_owned(), id: None, properties: HashMap::new(), }, @@ -120,7 +121,7 @@ fn missing_tenant_context_and_subject_property_is_denied() { name: "list".to_owned(), }, resource: Resource { - resource_type: "gts.cf.core.users.user.v1~".to_owned(), + resource_type: gts_id!("cf.core.users.user.v1~").to_owned(), id: None, properties: HashMap::new(), }, diff --git a/gears/system/authz-resolver/plugins/tr-authz-plugin/Cargo.toml b/gears/system/authz-resolver/plugins/tr-authz-plugin/Cargo.toml index b8119bfab..384c1a251 100644 --- a/gears/system/authz-resolver/plugins/tr-authz-plugin/Cargo.toml +++ b/gears/system/authz-resolver/plugins/tr-authz-plugin/Cargo.toml @@ -25,6 +25,7 @@ types-registry-sdk = { package = "cf-gears-types-registry-sdk", version = "0.3.0 # ToolKit dependencies toolkit = { workspace = true } +toolkit-gts = { workspace = true } toolkit-macros = { workspace = true } toolkit-security = { workspace = true } diff --git a/gears/system/authz-resolver/plugins/tr-authz-plugin/src/domain/service_tests.rs b/gears/system/authz-resolver/plugins/tr-authz-plugin/src/domain/service_tests.rs index 601aa71a3..2645b360a 100644 --- a/gears/system/authz-resolver/plugins/tr-authz-plugin/src/domain/service_tests.rs +++ b/gears/system/authz-resolver/plugins/tr-authz-plugin/src/domain/service_tests.rs @@ -13,6 +13,7 @@ use tenant_resolver_sdk::{ GetTenantsOptions, IsAncestorOptions, TenantId, TenantInfo, TenantRef, TenantResolverClient, TenantResolverError, TenantStatus, }; +use toolkit_gts::gts_id; use toolkit_security::SecurityContext; use uuid::Uuid; @@ -150,7 +151,7 @@ fn make_request(tenant_id: Uuid) -> EvaluationRequest { name: "list".to_owned(), }, resource: Resource { - resource_type: "gts.cf.test.v1~".to_owned(), + resource_type: gts_id!("cf.test.authz.resource.v1~").to_owned(), id: None, properties: std::collections::HashMap::default(), }, @@ -179,7 +180,7 @@ fn make_request_no_tenant() -> EvaluationRequest { name: "list".to_owned(), }, resource: Resource { - resource_type: "gts.cf.test.v1~".to_owned(), + resource_type: gts_id!("cf.test.authz.resource.v1~").to_owned(), id: None, properties: std::collections::HashMap::default(), }, @@ -456,7 +457,7 @@ fn build_r_request( name: action.to_owned(), }, resource: Resource { - resource_type: "gts.cf.test.v1~".to_owned(), + resource_type: gts_id!("cf.test.authz.resource.v1~").to_owned(), id: resource_id, properties: resource_props, }, diff --git a/gears/system/cluster/cluster-sdk/src/gts.rs b/gears/system/cluster/cluster-sdk/src/gts.rs index e38ea4e1d..2230fdbb4 100644 --- a/gears/system/cluster/cluster-sdk/src/gts.rs +++ b/gears/system/cluster/cluster-sdk/src/gts.rs @@ -54,7 +54,7 @@ use toolkit_gts::gts_type_schema; #[gts_type_schema( dir_path = "schemas", base = PluginV1, - type_id = "gts.cf.toolkit.plugins.plugin.v1~cf.core.cluster.plugin.v1~", + type_id = gts_id!("cf.toolkit.plugins.plugin.v1~cf.core.cluster.plugin.v1~"), description = "Cluster plugin specification", properties = "", )] diff --git a/gears/system/nodes-registry/nodes-registry-sdk/Cargo.toml b/gears/system/nodes-registry/nodes-registry-sdk/Cargo.toml index c3f64db1f..faf3a226f 100644 --- a/gears/system/nodes-registry/nodes-registry-sdk/Cargo.toml +++ b/gears/system/nodes-registry/nodes-registry-sdk/Cargo.toml @@ -24,6 +24,7 @@ thiserror = { workspace = true } uuid = { workspace = true } toolkit-canonical-errors = { workspace = true } +toolkit-gts = { workspace = true } toolkit-node-info = { workspace = true } [dev-dependencies] diff --git a/gears/system/nodes-registry/nodes-registry-sdk/src/error_tests.rs b/gears/system/nodes-registry/nodes-registry-sdk/src/error_tests.rs index 9b8869d92..563c9fc40 100644 --- a/gears/system/nodes-registry/nodes-registry-sdk/src/error_tests.rs +++ b/gears/system/nodes-registry/nodes-registry-sdk/src/error_tests.rs @@ -25,7 +25,7 @@ mod wire_vocabulary_round_trip { // Test scope mirroring the impl crate's `#[resource_error]` marker. // Its literal MUST equal `gts::NODE_RESOURCE_TYPE` — the // `node_resource_type_round_trip` test asserts that equality. - #[resource_error("gts.cf.nodes_registry.registry.node.v1~")] + #[resource_error(gts_id!("cf.nodes_registry.registry.node.v1~"))] struct NodeScope; fn problem(err: CanonicalError) -> serde_json::Value { @@ -76,7 +76,7 @@ mod projection_tests { use crate::{field, gts}; use toolkit_canonical_errors::{CanonicalError, resource_error}; - #[resource_error("gts.cf.nodes_registry.registry.node.v1~")] + #[resource_error(gts_id!("cf.nodes_registry.registry.node.v1~"))] struct NodeScope; #[test] diff --git a/gears/system/nodes-registry/nodes-registry-sdk/src/gts.rs b/gears/system/nodes-registry/nodes-registry-sdk/src/gts.rs index 562c9f585..55a8aa8b3 100644 --- a/gears/system/nodes-registry/nodes-registry-sdk/src/gts.rs +++ b/gears/system/nodes-registry/nodes-registry-sdk/src/gts.rs @@ -18,6 +18,7 @@ //! `resource_type` equals this constant, so a divergence trips at test //! time, not in production. +use toolkit_gts::gts_id; /// Nodes-registry node resource. Surfaces as the `resource_type` on the /// canonical `NotFound` raised when a node id does not resolve (404). -pub const NODE_RESOURCE_TYPE: &str = "gts.cf.nodes_registry.registry.node.v1~"; +pub const NODE_RESOURCE_TYPE: &str = gts_id!("cf.nodes_registry.registry.node.v1~"); diff --git a/gears/system/nodes-registry/nodes-registry/Cargo.toml b/gears/system/nodes-registry/nodes-registry/Cargo.toml index dd8fbb424..c0f7f05f6 100644 --- a/gears/system/nodes-registry/nodes-registry/Cargo.toml +++ b/gears/system/nodes-registry/nodes-registry/Cargo.toml @@ -34,3 +34,6 @@ toolkit-canonical-errors = { workspace = true, features = ["axum"] } toolkit-node-info = { workspace = true } toolkit-macros = { workspace = true } nodes_registry-sdk = { package = "cf-gears-nodes-registry-sdk", version = "0.1.22", path = "../nodes-registry-sdk" } + +[dev-dependencies] +toolkit-gts = { workspace = true } diff --git a/gears/system/nodes-registry/nodes-registry/src/api/rest/error.rs b/gears/system/nodes-registry/nodes-registry/src/api/rest/error.rs index 05102a8c6..fd59be427 100644 --- a/gears/system/nodes-registry/nodes-registry/src/api/rest/error.rs +++ b/gears/system/nodes-registry/nodes-registry/src/api/rest/error.rs @@ -1,7 +1,7 @@ use crate::domain::error::DomainError; use toolkit_canonical_errors::{CanonicalError, resource_error}; -#[resource_error("gts.cf.nodes_registry.registry.node.v1~")] +#[resource_error(gts_id!("cf.nodes_registry.registry.node.v1~"))] pub struct NodeError; impl From for CanonicalError { diff --git a/gears/system/nodes-registry/nodes-registry/tests/error_tests.rs b/gears/system/nodes-registry/nodes-registry/tests/error_tests.rs index fdf172a89..799558e29 100644 --- a/gears/system/nodes-registry/nodes-registry/tests/error_tests.rs +++ b/gears/system/nodes-registry/nodes-registry/tests/error_tests.rs @@ -10,6 +10,9 @@ use nodes_registry::domain::error::DomainError; use toolkit_canonical_errors::{CanonicalError, Problem}; +use toolkit_gts::GTS_ID_URI_PREFIX; +use toolkit_gts::gts_id; +use toolkit_gts::gts_uri; /// Build the wire `Problem` the canonical error middleware would emit /// for a given `DomainError`. @@ -29,19 +32,19 @@ fn test_error_conversion_mapping() { DomainError::NodeNotFound(test_id), 404, Some(test_id.to_string()), - "gts://gts.cf.core.errors.err.v1~cf.core.err.not_found.v1~", + gts_uri!("cf.core.errors.err.v1~cf.core.err.not_found.v1~"), ), ( DomainError::SysInfoCollectionFailed("Failed to read CPU info".to_owned()), 500, None, - "gts://gts.cf.core.errors.err.v1~cf.core.err.internal.v1~", + gts_uri!("cf.core.errors.err.v1~cf.core.err.internal.v1~"), ), ( DomainError::SysCapCollectionFailed("GPU detection failed".to_owned()), 500, None, - "gts://gts.cf.core.errors.err.v1~cf.core.err.internal.v1~", + gts_uri!("cf.core.errors.err.v1~cf.core.err.internal.v1~"), ), ( DomainError::InvalidInput("Invalid capability key format".to_owned()), @@ -49,13 +52,13 @@ fn test_error_conversion_mapping() { // InvalidArgument + with_field_violation sets a generic top-level detail; // caller-supplied text lives in context.field_violations[0].description. None, - "gts://gts.cf.core.errors.err.v1~cf.core.err.invalid_argument.v1~", + gts_uri!("cf.core.errors.err.v1~cf.core.err.invalid_argument.v1~"), ), ( DomainError::Internal("Database connection lost".to_owned()), 500, None, - "gts://gts.cf.core.errors.err.v1~cf.core.err.internal.v1~", + gts_uri!("cf.core.errors.err.v1~cf.core.err.internal.v1~"), ), ]; @@ -81,7 +84,7 @@ fn test_error_conversion_mapping() { at the conversion layer it stays None" ); assert!( - problem.problem_type.starts_with("gts://"), + problem.problem_type.starts_with(GTS_ID_URI_PREFIX), "Problem type should start with gts://" ); } @@ -96,7 +99,7 @@ fn test_node_not_found_sets_resource_type() { .get("resource_type") .and_then(|v| v.as_str()) .unwrap_or(""); - assert_eq!(rt, "gts.cf.nodes_registry.registry.node.v1~"); + assert_eq!(rt, gts_id!("cf.nodes_registry.registry.node.v1~")); let rn = problem .context .get("resource_name") diff --git a/gears/system/oagw/oagw-sdk/Cargo.toml b/gears/system/oagw/oagw-sdk/Cargo.toml index c64f8f406..204b801bc 100644 --- a/gears/system/oagw/oagw-sdk/Cargo.toml +++ b/gears/system/oagw/oagw-sdk/Cargo.toml @@ -32,6 +32,7 @@ serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } tracing = { workspace = true } toolkit-canonical-errors = { workspace = true } +toolkit-gts = { workspace = true } toolkit-security = { workspace = true } axum = { workspace = true, features = ["ws"], optional = true } diff --git a/gears/system/oagw/oagw-sdk/src/error.rs b/gears/system/oagw/oagw-sdk/src/error.rs index cbf6b2833..117becb49 100644 --- a/gears/system/oagw/oagw-sdk/src/error.rs +++ b/gears/system/oagw/oagw-sdk/src/error.rs @@ -397,7 +397,7 @@ mod sdk_vocabulary_round_trip_tests { use crate::{field, quota, reason}; use toolkit_canonical_errors::{CanonicalError, Problem, resource_error}; - #[resource_error("gts.cf.core.oagw.proxy.v1~")] + #[resource_error(gts_id!("cf.core.oagw.proxy.v1~"))] struct ProxyScope; fn problem(err: CanonicalError) -> serde_json::Value { @@ -521,17 +521,18 @@ mod projection_tests { use super::*; use crate::{field, reason}; use toolkit_canonical_errors::{CanonicalError, resource_error}; + use toolkit_gts::gts_id; - #[resource_error("gts.cf.core.oagw.proxy.v1~")] + #[resource_error(gts_id!("cf.core.oagw.proxy.v1~"))] struct ProxyScope; - #[resource_error("gts.cf.core.oagw.upstream.v1~")] + #[resource_error(gts_id!("cf.core.oagw.upstream.v1~"))] struct UpstreamScope; - #[resource_error("gts.cf.core.oagw.route.v1~")] + #[resource_error(gts_id!("cf.core.oagw.route.v1~"))] struct RouteScope; - #[resource_error("gts.cf.core.oagw.auth_plugin.v1~")] + #[resource_error(gts_id!("cf.core.oagw.auth_plugin.v1~"))] struct AuthPluginScope; #[test] @@ -777,7 +778,9 @@ mod projection_tests { } let canonical = AuthPluginScope::not_found("plugin gone") - .with_resource("gts.cf.core.oagw.auth_plugin.v1~cf.core.oagw.apikey.v1") + .with_resource(gts_id!( + "cf.core.oagw.auth_plugin.v1~cf.core.oagw.apikey.v1" + )) .create(); match ServiceGatewayError::from(canonical) { ServiceGatewayError::NotFound { resource, .. } => { @@ -805,7 +808,7 @@ mod projection_tests { fn unknown_resource_scope_preserves_wire_string() { // If the impl ever emits a NotFound under a resource scope the // SDK doesn't model, the projection preserves the raw string. - #[resource_error("gts.cf.future.oagw.something_new.v1~")] + #[resource_error(gts_id!("cf.future.oagw.something_new.v1~"))] struct FutureScope; let canonical = FutureScope::not_found("???") @@ -815,7 +818,7 @@ mod projection_tests { ServiceGatewayError::NotFound { resource: Resource::Unknown(s), .. - } => assert_eq!(s, "gts.cf.future.oagw.something_new.v1~"), + } => assert_eq!(s, gts_id!("cf.future.oagw.something_new.v1~")), other => panic!("expected NotFound::Unknown, got {other:?}"), } } diff --git a/gears/system/oagw/oagw-sdk/src/gts.rs b/gears/system/oagw/oagw-sdk/src/gts.rs index ff1e9f351..48f983468 100644 --- a/gears/system/oagw/oagw-sdk/src/gts.rs +++ b/gears/system/oagw/oagw-sdk/src/gts.rs @@ -23,23 +23,34 @@ //! } //! ``` +use toolkit_gts::gts_id; /// Umbrella resource scope for gateway data-plane / proxy errors. -pub const PROXY_SCHEMA: &str = "gts.cf.core.oagw.proxy.v1~"; +pub const PROXY_SCHEMA: &str = gts_id!("cf.core.oagw.proxy.v1~"); /// Resource scope for a specific upstream definition. -pub const UPSTREAM_SCHEMA: &str = "gts.cf.core.oagw.upstream.v1~"; +pub const UPSTREAM_SCHEMA: &str = gts_id!("cf.core.oagw.upstream.v1~"); /// Resource scope for a specific route definition. -pub const ROUTE_SCHEMA: &str = "gts.cf.core.oagw.route.v1~"; +pub const ROUTE_SCHEMA: &str = gts_id!("cf.core.oagw.route.v1~"); /// Resource scope for a specific authentication plugin instance. -pub const AUTH_PLUGIN_SCHEMA: &str = "gts.cf.core.oagw.auth_plugin.v1~"; +pub const AUTH_PLUGIN_SCHEMA: &str = gts_id!("cf.core.oagw.auth_plugin.v1~"); /// Resource scope for errors raised by guard plugins. -pub const GUARD_PLUGIN_SCHEMA: &str = "gts.cf.core.oagw.guard_plugin.v1~"; +pub const GUARD_PLUGIN_SCHEMA: &str = gts_id!("cf.core.oagw.guard_plugin.v1~"); /// Resource scope for a specific transform plugin instance. -pub const TRANSFORM_PLUGIN_SCHEMA: &str = "gts.cf.core.oagw.transform_plugin.v1~"; +pub const TRANSFORM_PLUGIN_SCHEMA: &str = gts_id!("cf.core.oagw.transform_plugin.v1~"); + +/// Resource scope for the protocol configuration type. +pub const PROTOCOL_SCHEMA: &str = gts_id!("cf.core.oagw.protocol.v1~"); + +/// Built-in HTTP protocol instance used by OAGW upstreams and routes. +pub const HTTP_PROTOCOL_ID: &str = gts_id!("cf.core.oagw.protocol.v1~cf.core.oagw.http.v1"); + +/// Built-in API-key auth plugin instance used by examples and defaults. +pub const APIKEY_AUTH_PLUGIN_ID: &str = + gts_id!("cf.core.oagw.auth_plugin.v1~cf.core.oagw.apikey.v1"); // --------------------------------------------------------------------------- // Typed view of the wire `resource_type` strings above. @@ -114,6 +125,7 @@ impl Resource { #[cfg(test)] mod tests { use super::*; + use toolkit_gts::GTS_ID_PREFIX; /// Sanity-check the literal shape every constant must satisfy — caught at /// compile time by the `#[resource_error]` macro on the impl side, but @@ -128,7 +140,7 @@ mod tests { GUARD_PLUGIN_SCHEMA, TRANSFORM_PLUGIN_SCHEMA, ] { - assert!(s.starts_with("gts."), "missing gts. prefix: {s}"); + assert!(s.starts_with(GTS_ID_PREFIX), "missing GTS prefix: {s}"); assert!(s.ends_with('~'), "missing trailing ~: {s}"); assert!( s.contains(".oagw."), @@ -155,7 +167,7 @@ mod tests { #[test] fn resource_unknown_preserves_wire_string() { - let raw = "gts.cf.future.oagw.something_new.v1~"; + let raw = gts_id!("cf.future.oagw.something_new.v1~"); let kind = Resource::from_wire(raw); match &kind { Resource::Unknown(s) => assert_eq!(s, raw), diff --git a/gears/system/oagw/oagw-sdk/src/lib.rs b/gears/system/oagw/oagw-sdk/src/lib.rs index 25a8e94da..27d1e4031 100644 --- a/gears/system/oagw/oagw-sdk/src/lib.rs +++ b/gears/system/oagw/oagw-sdk/src/lib.rs @@ -27,6 +27,10 @@ pub use api::ServiceGatewayClientV1; pub use body::Body; pub use codec::Json; pub use error::{ServiceGatewayError, StreamingError}; +pub use gts::{ + APIKEY_AUTH_PLUGIN_ID, AUTH_PLUGIN_SCHEMA, GUARD_PLUGIN_SCHEMA, HTTP_PROTOCOL_ID, + PROTOCOL_SCHEMA, PROXY_SCHEMA, ROUTE_SCHEMA, TRANSFORM_PLUGIN_SCHEMA, UPSTREAM_SCHEMA, +}; pub use multipart::{MultipartBody, MultipartError, Part}; pub use sse::{FromServerEvent, ServerEvent, ServerEventsResponse, ServerEventsStream}; #[cfg(feature = "axum")] diff --git a/gears/system/oagw/oagw/Cargo.toml b/gears/system/oagw/oagw/Cargo.toml index 4c1e404b8..ea6e7ee76 100644 --- a/gears/system/oagw/oagw/Cargo.toml +++ b/gears/system/oagw/oagw/Cargo.toml @@ -37,6 +37,7 @@ oagw-sdk = { path = "../oagw-sdk", package="cf-gears-oagw-sdk", version = "0.5.3 toolkit = { workspace = true } toolkit-auth = { workspace = true } toolkit-canonical-errors = { workspace = true, features = ["axum"] } +toolkit-gts = { workspace = true } toolkit-http = { workspace = true } toolkit-security = { workspace = true } toolkit-macros = { workspace = true } diff --git a/gears/system/oagw/oagw/src/api/rest/error.rs b/gears/system/oagw/oagw/src/api/rest/error.rs index 166460ed8..cf7857b1e 100644 --- a/gears/system/oagw/oagw/src/api/rest/error.rs +++ b/gears/system/oagw/oagw/src/api/rest/error.rs @@ -83,27 +83,27 @@ const RETRY_AFTER_GUARD_SECS: u64 = 10; /// Errors attributable to the gateway data-plane / proxy as a resource. /// Used as the umbrella scope when no more specific resource applies. -#[resource_error("gts.cf.core.oagw.proxy.v1~")] +#[resource_error(gts_id!("cf.core.oagw.proxy.v1~"))] pub struct OagwProxyError; /// Errors attributable to a specific upstream definition. -#[resource_error("gts.cf.core.oagw.upstream.v1~")] +#[resource_error(gts_id!("cf.core.oagw.upstream.v1~"))] pub struct OagwUpstreamError; /// Errors attributable to a specific route definition. -#[resource_error("gts.cf.core.oagw.route.v1~")] +#[resource_error(gts_id!("cf.core.oagw.route.v1~"))] pub struct OagwRouteError; /// Errors attributable to a specific authentication plugin instance. -#[resource_error("gts.cf.core.oagw.auth_plugin.v1~")] +#[resource_error(gts_id!("cf.core.oagw.auth_plugin.v1~"))] pub struct OagwAuthPluginError; /// Errors raised by guard plugins during the proxy pipeline. -#[resource_error("gts.cf.core.oagw.guard_plugin.v1~")] +#[resource_error(gts_id!("cf.core.oagw.guard_plugin.v1~"))] pub struct OagwGuardPluginError; /// Errors attributable to a specific transform plugin instance. -#[resource_error("gts.cf.core.oagw.transform_plugin.v1~")] +#[resource_error(gts_id!("cf.core.oagw.transform_plugin.v1~"))] pub struct OagwTransformPluginError; // --------------------------------------------------------------------------- @@ -537,6 +537,7 @@ pub fn error_response(err: DomainError) -> Response { #[cfg(test)] mod tests { use super::*; + use toolkit_gts::{gts_id, gts_uri}; /// Build the wire `Problem` the canonical error middleware would emit /// for a given `DomainError`. Tests run without the middleware in @@ -555,13 +556,13 @@ mod tests { } } - const NOT_FOUND_TYPE: &str = "gts://gts.cf.core.errors.err.v1~cf.core.err.not_found.v1~"; + const NOT_FOUND_TYPE: &str = gts_uri!("cf.core.errors.err.v1~cf.core.err.not_found.v1~"); const INVALID_ARGUMENT_TYPE: &str = - "gts://gts.cf.core.errors.err.v1~cf.core.err.invalid_argument.v1~"; + gts_uri!("cf.core.errors.err.v1~cf.core.err.invalid_argument.v1~"); const ALREADY_EXISTS_TYPE: &str = - "gts://gts.cf.core.errors.err.v1~cf.core.err.already_exists.v1~"; + gts_uri!("cf.core.errors.err.v1~cf.core.err.already_exists.v1~"); const RESOURCE_EXHAUSTED_TYPE: &str = - "gts://gts.cf.core.errors.err.v1~cf.core.err.resource_exhausted.v1~"; + gts_uri!("cf.core.errors.err.v1~cf.core.err.resource_exhausted.v1~"); #[test] fn validation_with_field_emits_field_violation() { @@ -666,7 +667,7 @@ mod tests { #[test] fn plugin_not_found_dispatches_on_gts_prefix() { let auth = DomainError::PluginNotFound { - gts_id: "gts.cf.core.oagw.auth_plugin.v1~cf.core.oagw.apikey.v1".into(), + gts_id: gts_id!("cf.core.oagw.auth_plugin.v1~cf.core.oagw.apikey.v1").into(), detail: "auth plugin not registered".into(), }; let p: Problem = auth.into_test_problem(); @@ -674,18 +675,18 @@ mod tests { assert_eq!(p.context["resource_type"], gts::AUTH_PLUGIN_SCHEMA); assert_eq!( p.context["resource_name"], - "gts.cf.core.oagw.auth_plugin.v1~cf.core.oagw.apikey.v1" + gts_id!("cf.core.oagw.auth_plugin.v1~cf.core.oagw.apikey.v1") ); let guard = DomainError::PluginNotFound { - gts_id: "gts.cf.core.oagw.guard_plugin.v1~cf.core.oagw.timeout.v1".into(), + gts_id: gts_id!("cf.core.oagw.guard_plugin.v1~cf.core.oagw.timeout.v1").into(), detail: "guard plugin not registered".into(), }; let p: Problem = guard.into_test_problem(); assert_eq!(p.context["resource_type"], gts::GUARD_PLUGIN_SCHEMA); let xform = DomainError::PluginInUse { - gts_id: "gts.cf.core.oagw.transform_plugin.v1~cf.core.oagw.logging.v1".into(), + gts_id: gts_id!("cf.core.oagw.transform_plugin.v1~cf.core.oagw.logging.v1").into(), detail: "transform plugin in use".into(), }; let p: Problem = xform.into_test_problem(); @@ -915,11 +916,11 @@ mod tests { instance: "/test".into(), }, DomainError::PluginNotFound { - gts_id: "gts.cf.core.oagw.auth_plugin.v1~cf.core.oagw.apikey.v1".into(), + gts_id: gts_id!("cf.core.oagw.auth_plugin.v1~cf.core.oagw.apikey.v1").into(), detail: "test".into(), }, DomainError::PluginInUse { - gts_id: "gts.cf.core.oagw.guard_plugin.v1~cf.core.oagw.timeout.v1".into(), + gts_id: gts_id!("cf.core.oagw.guard_plugin.v1~cf.core.oagw.timeout.v1").into(), detail: "test".into(), }, DomainError::Forbidden { @@ -1058,7 +1059,7 @@ mod tests { assert_eq!(p.status, 400); assert_eq!( p.problem_type, - "gts://gts.cf.core.errors.err.v1~cf.core.err.failed_precondition.v1~" + gts_uri!("cf.core.errors.err.v1~cf.core.err.failed_precondition.v1~") ); // resource_name must be absent — guard didn't supply an id. assert!(p.context.get("resource_name").is_none()); @@ -1093,7 +1094,7 @@ mod tests { assert_eq!(p.status, 409); assert_eq!( p.problem_type, - "gts://gts.cf.core.errors.err.v1~cf.core.err.aborted.v1~" + gts_uri!("cf.core.errors.err.v1~cf.core.err.aborted.v1~") ); assert!(p.context.get("resource_name").is_none()); } @@ -1117,7 +1118,7 @@ mod tests { assert_eq!(p.status, 401); assert_eq!( p.problem_type, - "gts://gts.cf.core.errors.err.v1~cf.core.err.unauthenticated.v1~" + gts_uri!("cf.core.errors.err.v1~cf.core.err.unauthenticated.v1~") ); assert_eq!( p.context["reason"], auth_reason, diff --git a/gears/system/oagw/oagw/src/api/rest/extractors.rs b/gears/system/oagw/oagw/src/api/rest/extractors.rs index b3a9716c9..375e49399 100644 --- a/gears/system/oagw/oagw/src/api/rest/extractors.rs +++ b/gears/system/oagw/oagw/src/api/rest/extractors.rs @@ -16,11 +16,12 @@ pub fn parse_gts_id(gts_str: &str, expected_schema: &str, instance: &str) -> Res let (schema, uuid) = gts_helpers::parse_resource_gts(gts_str) .map_err(|e| domain_error_to_problem(e, instance))?; let expected_prefix = expected_schema.trim_end_matches('~'); - if schema != expected_prefix { + let actual_prefix = schema.trim_end_matches('~'); + if actual_prefix != expected_prefix { let err = DomainError::Validation { field: "gts_id", reason: field::INVALID_GTS_SCHEMA, - detail: format!("expected GTS schema '{expected_schema}' but got '{schema}~'"), + detail: format!("expected GTS schema '{expected_schema}' but got '{schema}'"), instance: instance.to_string(), }; return Err(domain_error_to_problem(err, instance)); diff --git a/gears/system/oagw/oagw/src/api/rest/routes/mod.rs b/gears/system/oagw/oagw/src/api/rest/routes/mod.rs index 43aec7f6c..466ad7c8f 100644 --- a/gears/system/oagw/oagw/src/api/rest/routes/mod.rs +++ b/gears/system/oagw/oagw/src/api/rest/routes/mod.rs @@ -1,6 +1,7 @@ use axum::Router; use toolkit::api::OpenApiRegistry; use toolkit::api::operation_builder::LicenseFeature; +use toolkit_gts::gts_id; use crate::gear::AppState; @@ -8,11 +9,13 @@ mod proxy; mod route; mod upstream; +const OAGW_BASE_LICENSE_FEATURE: &str = gts_id!("cf.core.lic.feat.v1~cf.core.oagw.base.v1"); + pub(super) struct License; impl AsRef for License { fn as_ref(&self) -> &'static str { - "gts.cf.core.lic.feat.v1~cf.core.oagw.base.v1" + OAGW_BASE_LICENSE_FEATURE } } diff --git a/gears/system/oagw/oagw/src/domain/gts_helpers.rs b/gears/system/oagw/oagw/src/domain/gts_helpers.rs index 175aa6fd2..6f3e15595 100644 --- a/gears/system/oagw/oagw/src/domain/gts_helpers.rs +++ b/gears/system/oagw/oagw/src/domain/gts_helpers.rs @@ -5,6 +5,7 @@ use crate::domain::error::DomainError; use oagw_sdk::field; +use toolkit_gts::gts_id; use uuid::Uuid; // -- Schema GTS identifiers -- @@ -18,36 +19,38 @@ pub use oagw_sdk::gts::{ UPSTREAM_SCHEMA, }; -pub const PROTOCOL_SCHEMA: &str = "gts.cf.core.oagw.protocol.v1~"; +pub const PROTOCOL_SCHEMA: &str = gts_id!("cf.core.oagw.protocol.v1~"); // -- Builtin protocol instances -- -pub const HTTP_PROTOCOL_ID: &str = "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1"; -pub const GRPC_PROTOCOL_ID: &str = "gts.cf.core.oagw.protocol.v1~cf.core.oagw.grpc.v1"; +pub const HTTP_PROTOCOL_ID: &str = gts_id!("cf.core.oagw.protocol.v1~cf.core.oagw.http.v1"); +pub const GRPC_PROTOCOL_ID: &str = gts_id!("cf.core.oagw.protocol.v1~cf.core.oagw.grpc.v1"); // -- Builtin auth plugin instances -- -pub const NOOP_AUTH_PLUGIN_ID: &str = "gts.cf.core.oagw.auth_plugin.v1~cf.core.oagw.noop.v1"; -pub const APIKEY_AUTH_PLUGIN_ID: &str = "gts.cf.core.oagw.auth_plugin.v1~cf.core.oagw.apikey.v1"; -pub const BASIC_AUTH_PLUGIN_ID: &str = "gts.cf.core.oagw.auth_plugin.v1~cf.core.oagw.basic.v1"; -pub const BEARER_AUTH_PLUGIN_ID: &str = "gts.cf.core.oagw.auth_plugin.v1~cf.core.oagw.bearer.v1"; +pub const NOOP_AUTH_PLUGIN_ID: &str = gts_id!("cf.core.oagw.auth_plugin.v1~cf.core.oagw.noop.v1"); +pub const APIKEY_AUTH_PLUGIN_ID: &str = + gts_id!("cf.core.oagw.auth_plugin.v1~cf.core.oagw.apikey.v1"); +pub const BASIC_AUTH_PLUGIN_ID: &str = gts_id!("cf.core.oagw.auth_plugin.v1~cf.core.oagw.basic.v1"); +pub const BEARER_AUTH_PLUGIN_ID: &str = + gts_id!("cf.core.oagw.auth_plugin.v1~cf.core.oagw.bearer.v1"); pub const OAUTH2_CLIENT_CRED_AUTH_PLUGIN_ID: &str = - "gts.cf.core.oagw.auth_plugin.v1~cf.core.oagw.oauth2_client_cred.v1"; + gts_id!("cf.core.oagw.auth_plugin.v1~cf.core.oagw.oauth2_client_cred.v1"); pub const OAUTH2_CLIENT_CRED_BASIC_AUTH_PLUGIN_ID: &str = - "gts.cf.core.oagw.auth_plugin.v1~cf.core.oagw.oauth2_client_cred_basic.v1"; + gts_id!("cf.core.oagw.auth_plugin.v1~cf.core.oagw.oauth2_client_cred_basic.v1"); // -- Builtin guard plugin instances -- pub const TIMEOUT_GUARD_PLUGIN_ID: &str = - "gts.cf.core.oagw.guard_plugin.v1~cf.core.oagw.timeout.v1"; -pub const CORS_GUARD_PLUGIN_ID: &str = "gts.cf.core.oagw.guard_plugin.v1~cf.core.oagw.cors.v1"; + gts_id!("cf.core.oagw.guard_plugin.v1~cf.core.oagw.timeout.v1"); +pub const CORS_GUARD_PLUGIN_ID: &str = gts_id!("cf.core.oagw.guard_plugin.v1~cf.core.oagw.cors.v1"); pub const REQUIRED_HEADERS_GUARD_PLUGIN_ID: &str = - "gts.cf.core.oagw.guard_plugin.v1~cf.core.oagw.required_headers.v1"; + gts_id!("cf.core.oagw.guard_plugin.v1~cf.core.oagw.required_headers.v1"); // -- Builtin transform plugin instances -- pub const LOGGING_TRANSFORM_PLUGIN_ID: &str = - "gts.cf.core.oagw.transform_plugin.v1~cf.core.oagw.logging.v1"; + gts_id!("cf.core.oagw.transform_plugin.v1~cf.core.oagw.logging.v1"); pub const METRICS_TRANSFORM_PLUGIN_ID: &str = - "gts.cf.core.oagw.transform_plugin.v1~cf.core.oagw.metrics.v1"; + gts_id!("cf.core.oagw.transform_plugin.v1~cf.core.oagw.metrics.v1"); pub const REQUEST_ID_TRANSFORM_PLUGIN_ID: &str = - "gts.cf.core.oagw.transform_plugin.v1~cf.core.oagw.request_id.v1"; + gts_id!("cf.core.oagw.transform_plugin.v1~cf.core.oagw.request_id.v1"); /// Format an upstream resource as a GTS identifier. #[must_use] @@ -63,32 +66,32 @@ pub fn format_route_gts(id: Uuid) -> String { /// Parse a resource GTS identifier, extracting the schema and UUID instance. /// -/// Validates the full identifier using the `gts` crate (0.8.4+ supports -/// anonymous UUID instance segments) and then splits at `~` to extract the -/// schema prefix and UUID. +/// Validates and decomposes the full identifier through the `gts` crate. pub fn parse_resource_gts(s: &str) -> Result<(String, Uuid), DomainError> { - // Validate the full GTS identifier (anonymous UUID segments supported since 0.8.4). - gts::GtsID::new(s).map_err(|e| DomainError::Validation { + let parsed = gts::GtsId::try_new(s).map_err(|e| DomainError::Validation { field: "gts_id", reason: field::INVALID_GTS_FORMAT, detail: format!("invalid GTS identifier: {e}"), instance: s.to_string(), })?; - let tilde_pos = s.rfind('~').ok_or_else(|| DomainError::Validation { - field: "gts_id", - reason: field::MISSING_GTS_TILDE, - detail: "missing '~' separator in GTS identifier".into(), - instance: s.to_string(), - })?; - - let instance = &s[tilde_pos + 1..]; - let uuid = Uuid::parse_str(instance).map_err(|e| DomainError::Validation { - field: "gts_id", - reason: field::INVALID_GTS_UUID, - detail: format!("invalid UUID in GTS instance: {e}"), - instance: s.to_string(), - })?; - - Ok((s[..tilde_pos].to_string(), uuid)) + let uuid = parsed + .segments() + .last() + .and_then(gts::GtsIdSegment::uuid) + .ok_or_else(|| DomainError::Validation { + field: "gts_id", + reason: field::INVALID_GTS_UUID, + detail: "GTS identifier must end with an anonymous UUID instance segment".into(), + instance: s.to_string(), + })?; + let schema = parsed + .get_type_id() + .ok_or_else(|| DomainError::Validation { + field: "gts_id", + reason: field::MISSING_GTS_TILDE, + detail: "missing type-schema segment in GTS identifier".into(), + instance: s.to_string(), + })?; + Ok((schema, uuid)) } diff --git a/gears/system/oagw/oagw/src/domain/services/management/bind.rs b/gears/system/oagw/oagw/src/domain/services/management/bind.rs index 4eebb9f1a..87180e8c4 100644 --- a/gears/system/oagw/oagw/src/domain/services/management/bind.rs +++ b/gears/system/oagw/oagw/src/domain/services/management/bind.rs @@ -4,11 +4,12 @@ use crate::domain::model::Upstream; use authz_resolver_sdk::PolicyEnforcer; use authz_resolver_sdk::pep::{AccessRequest, ResourceType}; use credstore_sdk::CredStoreClientV1; +use oagw_sdk::UPSTREAM_SCHEMA; use toolkit_security::SecurityContext; /// Resource type for upstream binding permission checks. const UPSTREAM_RESOURCE: ResourceType = - ResourceType::from_static("gts.cf.core.oagw.upstream.v1~", &["owner_tenant_id"]); + ResourceType::from_static(UPSTREAM_SCHEMA, &["owner_tenant_id"]); /// Permission action names for ancestor bind checks. pub(in crate::domain::services) mod bind_actions { diff --git a/gears/system/oagw/oagw/src/domain/services/management/tests.rs b/gears/system/oagw/oagw/src/domain/services/management/tests.rs index 81669a51a..01ed169f5 100644 --- a/gears/system/oagw/oagw/src/domain/services/management/tests.rs +++ b/gears/system/oagw/oagw/src/domain/services/management/tests.rs @@ -10,6 +10,7 @@ use crate::domain::test_support::{ MockCredStoreClient, MockTenantResolverClient, allow_all_enforcer, }; use crate::infra::storage::{InMemoryRouteRepo, InMemoryUpstreamRepo}; +use oagw_sdk::HTTP_PROTOCOL_ID; use tenant_resolver_sdk::TenantId; fn make_service() -> ControlPlaneServiceImpl { @@ -67,7 +68,7 @@ fn make_create_upstream_hostname() -> CreateUpstreamRequest { port: 443, }], }, - protocol: "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1".into(), + protocol: HTTP_PROTOCOL_ID.into(), alias: None, auth: None, headers: None, @@ -90,7 +91,7 @@ fn make_create_upstream_ip(alias: &str) -> CreateUpstreamRequest { port: 443, }], }, - protocol: "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1".into(), + protocol: HTTP_PROTOCOL_ID.into(), alias: Some(alias.into()), auth: None, headers: None, @@ -221,7 +222,7 @@ async fn alias_auto_generation() { port: 8443, }], }, - protocol: "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1".into(), + protocol: HTTP_PROTOCOL_ID.into(), alias: None, auth: None, headers: None, @@ -2969,7 +2970,7 @@ async fn create_ip_requires_explicit_alias() { port: 443, }], }, - protocol: "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1".into(), + protocol: HTTP_PROTOCOL_ID.into(), alias: None, auth: None, headers: None, @@ -3133,7 +3134,7 @@ async fn multi_endpoint_common_suffix_alias() { }, ], }, - protocol: "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1".into(), + protocol: HTTP_PROTOCOL_ID.into(), alias: None, auth: None, headers: None, @@ -3170,7 +3171,7 @@ async fn multi_endpoint_same_suffix_different_ports_get_distinct_aliases() { }, ], }, - protocol: "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1".into(), + protocol: HTTP_PROTOCOL_ID.into(), alias: None, auth: None, headers: None, @@ -3200,7 +3201,7 @@ async fn multi_endpoint_same_suffix_different_ports_get_distinct_aliases() { }, ], }, - protocol: "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1".into(), + protocol: HTTP_PROTOCOL_ID.into(), alias: None, auth: None, headers: None, @@ -3241,7 +3242,7 @@ async fn multi_endpoint_public_suffix_requires_explicit_alias() { }, ], }, - protocol: "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1".into(), + protocol: HTTP_PROTOCOL_ID.into(), alias: None, auth: None, headers: None, @@ -3283,7 +3284,7 @@ async fn multi_endpoint_public_suffix_with_explicit_alias_succeeds() { }, ], }, - protocol: "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1".into(), + protocol: HTTP_PROTOCOL_ID.into(), alias: Some("my-uk-backends".into()), auth: None, headers: None, @@ -4753,7 +4754,7 @@ fn make_create_upstream_with_ip(ip: &str, alias: &str) -> CreateUpstreamRequest port: 443, }], }, - protocol: "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1".into(), + protocol: HTTP_PROTOCOL_ID.into(), alias: Some(alias.into()), auth: None, headers: None, diff --git a/gears/system/oagw/oagw/src/domain/type_catalog.rs b/gears/system/oagw/oagw/src/domain/type_catalog.rs index 5928591db..78b5c5cb1 100644 --- a/gears/system/oagw/oagw/src/domain/type_catalog.rs +++ b/gears/system/oagw/oagw/src/domain/type_catalog.rs @@ -4,6 +4,7 @@ //! ready for `TypesRegistryClient::register()`. use serde_json::{Value, json}; +use toolkit_gts::gts_uri; use super::gts_helpers::*; @@ -13,7 +14,7 @@ use super::gts_helpers::*; /// a schema (type definition) rather than an instance. fn schema_entity(gts_id: &str, description: &str) -> Value { json!({ - "$id": format!("gts://{gts_id}"), + "$id": gts_uri!(gts_id), "$schema": "http://json-schema.org/draft-07/schema#", "description": description, "type": "object" @@ -72,10 +73,11 @@ pub fn oagw_gts_entities() -> Vec { #[cfg(test)] mod tests { use super::*; + use toolkit_gts::GTS_ID_URI_PREFIX; /// Strip the `gts://` URI prefix that schema `$id` fields use. fn strip_gts_uri(id: &str) -> &str { - id.strip_prefix("gts://").unwrap_or(id) + id.strip_prefix(GTS_ID_URI_PREFIX).unwrap_or(id) } #[test] @@ -96,8 +98,8 @@ mod tests { .unwrap_or_else(|| panic!("entity missing $id: {entity}")); let id = strip_gts_uri(raw_id); assert!( - id.starts_with("gts."), - "GTS ID should start with 'gts.': {id}" + gts::GtsId::try_new(id).is_ok(), + "GTS ID should parse as a concrete GTS ID: {id}" ); } } @@ -108,7 +110,7 @@ mod tests { if entity.get("$schema").is_some() { let id = entity["$id"].as_str().unwrap(); assert!( - id.starts_with("gts://"), + id.starts_with(GTS_ID_URI_PREFIX), "Schema $id must use gts:// URI format: {id}" ); } @@ -137,7 +139,7 @@ mod tests { let raw_id = entity["$id"].as_str().unwrap(); let id = strip_gts_uri(raw_id); // Validate via the gts crate - gts::GtsID::new(id).unwrap_or_else(|e| panic!("invalid GTS ID '{id}': {e}")); + gts::GtsId::try_new(id).unwrap_or_else(|e| panic!("invalid GTS ID '{id}': {e}")); } } } diff --git a/gears/system/oagw/oagw/src/infra/plugin/registry.rs b/gears/system/oagw/oagw/src/infra/plugin/registry.rs index eb22092e3..c1e5cce86 100644 --- a/gears/system/oagw/oagw/src/infra/plugin/registry.rs +++ b/gears/system/oagw/oagw/src/infra/plugin/registry.rs @@ -161,6 +161,7 @@ mod tests { use std::sync::Arc; use crate::domain::test_support::MockCredStoreClient; + use toolkit_gts::gts_id; use super::*; @@ -203,7 +204,9 @@ mod tests { #[test] fn unknown_plugin_returns_error() { let registry = make_registry(); - let err = registry.resolve("gts.cf.core.oagw.auth_plugin.v1~cf.core.oagw.unknown.v1"); + let err = registry.resolve(gts_id!( + "cf.core.oagw.auth_plugin.v1~cf.core.oagw.unknown.v1" + )); assert!(err.is_err()); } @@ -216,7 +219,9 @@ mod tests { #[test] fn unknown_guard_plugin_returns_error() { let registry = GuardPluginRegistry::with_builtins(); - let err = registry.resolve("gts.cf.core.oagw.guard_plugin.v1~cf.core.oagw.unknown.v1"); + let err = registry.resolve(gts_id!( + "cf.core.oagw.guard_plugin.v1~cf.core.oagw.unknown.v1" + )); assert!(err.is_err()); } @@ -237,7 +242,9 @@ mod tests { #[test] fn unknown_transform_plugin_returns_error() { let registry = TransformPluginRegistry::with_builtins(); - let err = registry.resolve("gts.cf.core.oagw.transform_plugin.v1~cf.core.oagw.unknown.v1"); + let err = registry.resolve(gts_id!( + "cf.core.oagw.transform_plugin.v1~cf.core.oagw.unknown.v1" + )); assert!(err.is_err()); } diff --git a/gears/system/oagw/oagw/src/infra/proxy/mod.rs b/gears/system/oagw/oagw/src/infra/proxy/mod.rs index 2725e665b..f90f1491b 100644 --- a/gears/system/oagw/oagw/src/infra/proxy/mod.rs +++ b/gears/system/oagw/oagw/src/infra/proxy/mod.rs @@ -23,13 +23,12 @@ pub(crate) use service::DataPlaneServiceImpl; pub(crate) mod resources { use super::ResourceType; + use oagw_sdk::PROXY_SCHEMA; use toolkit_security::pep_properties; /// Resource type identifying a proxied upstream target. - pub const PROXY: ResourceType = ResourceType::from_static( - "gts.cf.core.oagw.proxy.v1~", - &[pep_properties::OWNER_TENANT_ID], - ); + pub const PROXY: ResourceType = + ResourceType::from_static(PROXY_SCHEMA, &[pep_properties::OWNER_TENANT_ID]); } pub(crate) mod actions { diff --git a/gears/system/oagw/oagw/src/infra/storage/upstream_repo.rs b/gears/system/oagw/oagw/src/infra/storage/upstream_repo.rs index d3bef9b45..8526c856a 100644 --- a/gears/system/oagw/oagw/src/infra/storage/upstream_repo.rs +++ b/gears/system/oagw/oagw/src/infra/storage/upstream_repo.rs @@ -172,6 +172,7 @@ impl UpstreamRepository for InMemoryUpstreamRepo { #[cfg(test)] mod tests { use crate::domain::model::{Endpoint, Scheme, Server}; + use oagw_sdk::HTTP_PROTOCOL_ID; use super::*; @@ -187,7 +188,7 @@ mod tests { port: 443, }], }, - protocol: "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1".into(), + protocol: HTTP_PROTOCOL_ID.into(), enabled: true, auth: None, headers: None, diff --git a/gears/system/oagw/oagw/src/infra/type_provisioning.rs b/gears/system/oagw/oagw/src/infra/type_provisioning.rs index 44f6f3480..cabd5ad19 100644 --- a/gears/system/oagw/oagw/src/infra/type_provisioning.rs +++ b/gears/system/oagw/oagw/src/infra/type_provisioning.rs @@ -776,6 +776,8 @@ impl TypeProvisioningService for TypeProvisioningServiceImpl { #[cfg(test)] mod tests { + use oagw_sdk::HTTP_PROTOCOL_ID; + use toolkit_gts::gts_id; use types_registry_sdk::{ GtsInstance, testing::{MockTypesRegistryClient, internal, make_test_instance}, @@ -797,7 +799,7 @@ mod tests { "server": { "endpoints": [{"host": "127.0.0.1", "port": 8080, "scheme": "http"}] }, - "protocol": "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + "protocol": HTTP_PROTOCOL_ID, "enabled": true, "tags": [] }) @@ -824,7 +826,7 @@ mod tests { let tenant = Uuid::new_v4(); let instance_id = Uuid::new_v4(); let content = upstream_content(tenant); - let gts_id = format!("gts.cf.core.oagw.upstream.v1~{instance_id}"); + let gts_id = format!("{}{}", gts_id!("cf.core.oagw.upstream.v1~"), instance_id); let registry = Arc::new( MockTypesRegistryClient::new() @@ -844,7 +846,7 @@ mod tests { let registry = Arc::new( MockTypesRegistryClient::new().with_instances([make_upstream_instance( - "gts.cf.core.oagw.upstream.v1~cf.core.oagw.test.v1", + gts_id!("cf.core.oagw.upstream.v1~cf.core.oagw.test.v1"), upstream_content(Uuid::new_v4()), )]), ); @@ -861,7 +863,7 @@ mod tests { #[tokio::test] async fn list_upstreams_rejects_invalid_content() { let instance_id = Uuid::new_v4(); - let gts_id = format!("gts.cf.core.oagw.upstream.v1~{instance_id}"); + let gts_id = format!("{}{}", gts_id!("cf.core.oagw.upstream.v1~"), instance_id); let registry = Arc::new( MockTypesRegistryClient::new().with_instances([make_upstream_instance( @@ -904,7 +906,7 @@ mod tests { let upstream_id = Uuid::new_v4(); let route_instance_id = Uuid::new_v4(); let content = route_content(tenant, upstream_id); - let gts_id = format!("gts.cf.core.oagw.route.v1~{route_instance_id}"); + let gts_id = format!("{}{}", gts_id!("cf.core.oagw.route.v1~"), route_instance_id); let registry = Arc::new( MockTypesRegistryClient::new().with_instances([make_route_instance(>s_id, content)]), @@ -924,7 +926,7 @@ mod tests { let registry = Arc::new( MockTypesRegistryClient::new().with_instances([make_route_instance( - "gts.cf.core.oagw.route.v1~cf.core.oagw.test.v1", + gts_id!("cf.core.oagw.route.v1~cf.core.oagw.test.v1"), route_content(Uuid::new_v4(), Uuid::new_v4()), )]), ); @@ -941,7 +943,7 @@ mod tests { #[tokio::test] async fn list_routes_rejects_invalid_content() { let instance_id = Uuid::new_v4(); - let gts_id = format!("gts.cf.core.oagw.route.v1~{instance_id}"); + let gts_id = format!("{}{}", gts_id!("cf.core.oagw.route.v1~"), instance_id); let registry = Arc::new( MockTypesRegistryClient::new().with_instances([make_route_instance( @@ -979,7 +981,7 @@ mod tests { assert_eq!(queries.len(), 1); assert_eq!( queries[0].pattern.as_deref(), - Some("gts.cf.core.oagw.upstream.v1~*") + Some(format!("{UPSTREAM_SCHEMA}*").as_str()) ); } @@ -993,7 +995,7 @@ mod tests { assert_eq!(queries.len(), 1); assert_eq!( queries[0].pattern.as_deref(), - Some("gts.cf.core.oagw.route.v1~*") + Some(format!("{ROUTE_SCHEMA}*").as_str()) ); } @@ -1012,7 +1014,7 @@ mod tests { {"scheme": "http", "host": "fallback.local", "port": 8080} ] }, - "protocol": "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + "protocol": HTTP_PROTOCOL_ID, "alias": "openai", "auth": { "type": "apikey", @@ -1039,10 +1041,7 @@ mod tests { assert_eq!(req.server.endpoints[0].host, "api.openai.com"); assert_eq!(req.server.endpoints[0].port, 443); assert_eq!(req.server.endpoints[1].scheme, domain::Scheme::Http); - assert_eq!( - req.protocol, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1" - ); + assert_eq!(req.protocol, HTTP_PROTOCOL_ID); assert_eq!(req.alias.as_deref(), Some("openai")); assert!(req.enabled); assert_eq!(req.tags, vec!["prod", "llm"]); @@ -1094,7 +1093,10 @@ mod tests { let payload: RoutePayload = serde_json::from_value(json).unwrap(); let route_uuid = Uuid::new_v4(); let provisioned = payload - .into_provisioned("gts.cf.core.oagw.route.v1~cf.core.oagw.test.v1", route_uuid) + .into_provisioned( + gts_id!("cf.core.oagw.route.v1~cf.core.oagw.test.v1"), + route_uuid, + ) .expect("upstream_id should parse"); assert_eq!(provisioned.tenant_id, Some(tenant)); @@ -1158,7 +1160,10 @@ mod tests { let payload: RoutePayload = serde_json::from_value(json).unwrap(); let route_uuid = Uuid::new_v4(); let provisioned = payload - .into_provisioned("gts.cf.core.oagw.route.v1~cf.core.oagw.test.v1", route_uuid) + .into_provisioned( + gts_id!("cf.core.oagw.route.v1~cf.core.oagw.test.v1"), + route_uuid, + ) .unwrap(); assert_eq!(provisioned.tenant_id, None); } @@ -1170,11 +1175,11 @@ mod tests { "server": { "endpoints": [{"host": "127.0.0.1", "port": 8080, "scheme": "http"}] }, - "protocol": "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + "protocol": HTTP_PROTOCOL_ID, "enabled": true, "tags": [] }); - let gts_id = format!("gts.cf.core.oagw.upstream.v1~{instance_id}"); + let gts_id = format!("{}{}", gts_id!("cf.core.oagw.upstream.v1~"), instance_id); let registry = Arc::new( MockTypesRegistryClient::new() diff --git a/gears/system/oagw/oagw/tests/e2e_http2_test.rs b/gears/system/oagw/oagw/tests/e2e_http2_test.rs index b7ca9f711..c4e283502 100644 --- a/gears/system/oagw/oagw/tests/e2e_http2_test.rs +++ b/gears/system/oagw/oagw/tests/e2e_http2_test.rs @@ -21,6 +21,7 @@ use hyper::service::service_fn; use hyper::{Request, Response}; use hyper_util::rt::{TokioExecutor, TokioIo}; use oagw::test_support::{AppHarness, test_server_config}; +use oagw_sdk::HTTP_PROTOCOL_ID; use tokio::net::TcpListener; use tokio::sync::Mutex; use tokio_rustls::TlsAcceptor; @@ -189,7 +190,7 @@ async fn e2e_http2_upstream_round_trip() { "scheme": "https" }] }, - "protocol": "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + "protocol": HTTP_PROTOCOL_ID, "alias": "e2e-h2", "enabled": true, "tags": [] @@ -262,7 +263,7 @@ async fn e2e_http2_upstream_get_no_body() { "scheme": "https" }] }, - "protocol": "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + "protocol": HTTP_PROTOCOL_ID, "alias": "e2e-h2-get", "enabled": true, "tags": [] @@ -328,7 +329,7 @@ async fn e2e_http2_upstream_large_body() { "scheme": "https" }] }, - "protocol": "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + "protocol": HTTP_PROTOCOL_ID, "alias": "e2e-h2-large", "enabled": true, "tags": [] @@ -402,7 +403,7 @@ async fn e2e_http2_upstream_sse_streaming() { "scheme": "https" }] }, - "protocol": "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + "protocol": HTTP_PROTOCOL_ID, "alias": "e2e-h2-sse", "enabled": true, "tags": [] diff --git a/gears/system/oagw/oagw/tests/e2e_smoke_test.rs b/gears/system/oagw/oagw/tests/e2e_smoke_test.rs index 521d9bc4d..b861602b0 100644 --- a/gears/system/oagw/oagw/tests/e2e_smoke_test.rs +++ b/gears/system/oagw/oagw/tests/e2e_smoke_test.rs @@ -6,11 +6,13 @@ use std::sync::Arc; use std::time::Duration; +use toolkit_gts::gts_uri; use oagw::test_support::{ APIKEY_AUTH_PLUGIN_ID, AppHarness, CapturingAuthZResolverClient, DenyingAuthZResolverClient, MockBody, MockGuard, MockResponse, }; +use oagw_sdk::{HTTP_PROTOCOL_ID, PROXY_SCHEMA}; // 10.1: E2E — create upstream, create route, proxy chat completion, verify round-trip. #[tokio::test] @@ -28,7 +30,7 @@ async fn e2e_chat_completion_round_trip() { "server": { "endpoints": [{"host": "127.0.0.1", "port": h.mock_port(), "scheme": "http"}] }, - "protocol": "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + "protocol": HTTP_PROTOCOL_ID, "alias": "e2e-openai", "enabled": true, "tags": [] @@ -100,7 +102,7 @@ async fn e2e_sse_streaming() { "server": { "endpoints": [{"host": "127.0.0.1", "port": h.mock_port(), "scheme": "http"}] }, - "protocol": "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + "protocol": HTTP_PROTOCOL_ID, "alias": "e2e-sse", "enabled": true, "tags": [] @@ -160,7 +162,7 @@ async fn e2e_auth_injection() { "server": { "endpoints": [{"host": "127.0.0.1", "port": h.mock_port(), "scheme": "http"}] }, - "protocol": "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + "protocol": HTTP_PROTOCOL_ID, "alias": "e2e-auth", "enabled": true, "tags": [], @@ -239,7 +241,7 @@ async fn e2e_disabled_upstream_returns_503() { "server": { "endpoints": [{"host": "127.0.0.1", "port": h.mock_port(), "scheme": "http"}] }, - "protocol": "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + "protocol": HTTP_PROTOCOL_ID, "alias": "e2e-disabled", "enabled": false, "tags": [] @@ -267,7 +269,7 @@ async fn e2e_upstream_500_passthrough() { "server": { "endpoints": [{"host": "127.0.0.1", "port": h.mock_port(), "scheme": "http"}] }, - "protocol": "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + "protocol": HTTP_PROTOCOL_ID, "alias": "e2e-errors", "enabled": true, "tags": [] @@ -317,7 +319,7 @@ async fn e2e_rate_limit_returns_429() { "server": { "endpoints": [{"host": "127.0.0.1", "port": h.mock_port(), "scheme": "http"}] }, - "protocol": "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + "protocol": HTTP_PROTOCOL_ID, "alias": "e2e-rl", "enabled": true, "tags": [], @@ -380,7 +382,7 @@ async fn e2e_management_lifecycle() { "server": { "endpoints": [{"host": "127.0.0.1", "port": h.mock_port(), "scheme": "http"}] }, - "protocol": "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + "protocol": HTTP_PROTOCOL_ID, "alias": "lifecycle", "enabled": true, "tags": [] @@ -409,7 +411,7 @@ async fn e2e_management_lifecycle() { {"host": "127.0.0.2", "port": h.mock_port(), "scheme": "http"} ] }, - "protocol": "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + "protocol": HTTP_PROTOCOL_ID, "alias": "lifecycle", "enabled": true, "tags": [] @@ -473,7 +475,7 @@ async fn e2e_invalid_content_length_returns_400() { "server": { "endpoints": [{"host": "127.0.0.1", "port": h.mock_port(), "scheme": "http"}] }, - "protocol": "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + "protocol": HTTP_PROTOCOL_ID, "alias": "e2e-cl", "enabled": true, "tags": [] @@ -526,7 +528,7 @@ async fn e2e_body_exceeding_limit_returns_400() { "server": { "endpoints": [{"host": "127.0.0.1", "port": h.mock_port(), "scheme": "http"}] }, - "protocol": "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + "protocol": HTTP_PROTOCOL_ID, "alias": "e2e-big", "enabled": true, "tags": [] @@ -593,7 +595,7 @@ async fn e2e_upstream_timeout_returns_504() { "server": { "endpoints": [{"host": "127.0.0.1", "port": h.mock_port(), "scheme": "http"}] }, - "protocol": "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + "protocol": HTTP_PROTOCOL_ID, "alias": "e2e-timeout", "enabled": true, "tags": [] @@ -646,7 +648,7 @@ async fn e2e_authz_forbidden_returns_403() { "server": { "endpoints": [{"host": "127.0.0.1", "port": h.mock_port(), "scheme": "http"}] }, - "protocol": "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + "protocol": HTTP_PROTOCOL_ID, "alias": "e2e-forbidden", "enabled": true, "tags": [] @@ -686,7 +688,7 @@ async fn e2e_authz_forbidden_returns_403() { assert_eq!(body["title"], "Permission Denied"); assert_eq!( body["type"], - "gts://gts.cf.core.errors.err.v1~cf.core.err.permission_denied.v1~" + gts_uri!("cf.core.errors.err.v1~cf.core.err.permission_denied.v1~") ); } @@ -718,7 +720,7 @@ async fn e2e_authz_request_carries_tenant_context() { "server": { "endpoints": [{"host": "127.0.0.1", "port": h.mock_port(), "scheme": "http"}] }, - "protocol": "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + "protocol": HTTP_PROTOCOL_ID, "alias": "e2e-authz-ctx", "enabled": true, "tags": [] @@ -767,7 +769,7 @@ async fn e2e_authz_request_carries_tenant_context() { Some(h.security_context().subject_tenant_id()), "tenant_context.root_id should match subject_tenant_id" ); - assert_eq!(req.resource.resource_type, "gts.cf.core.oagw.proxy.v1~"); + assert_eq!(req.resource.resource_type, PROXY_SCHEMA); assert_eq!(req.action.name, "invoke"); } @@ -789,7 +791,7 @@ async fn setup_rate_limited_upstream( "server": { "endpoints": [{"host": "127.0.0.1", "port": h.mock_port(), "scheme": "http"}] }, - "protocol": "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + "protocol": HTTP_PROTOCOL_ID, "alias": alias, "enabled": true, "tags": [], @@ -1143,7 +1145,7 @@ async fn e2e_rate_limit_route_level_enforcement() { "server": { "endpoints": [{"host": "127.0.0.1", "port": h.mock_port(), "scheme": "http"}] }, - "protocol": "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + "protocol": HTTP_PROTOCOL_ID, "alias": "e2e-rl-route", "enabled": true, "tags": [] @@ -1201,7 +1203,7 @@ async fn e2e_rate_limit_upstream_and_route_both_applied() { "server": { "endpoints": [{"host": "127.0.0.1", "port": h.mock_port(), "scheme": "http"}] }, - "protocol": "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + "protocol": HTTP_PROTOCOL_ID, "alias": "e2e-rl-dual", "enabled": true, "tags": [], diff --git a/gears/system/oagw/oagw/tests/management_api_test.rs b/gears/system/oagw/oagw/tests/management_api_test.rs index b2891da58..c76b46646 100644 --- a/gears/system/oagw/oagw/tests/management_api_test.rs +++ b/gears/system/oagw/oagw/tests/management_api_test.rs @@ -5,6 +5,9 @@ #![cfg(not(feature = "fips"))] use oagw::test_support::{AppHarness, format_upstream_gts}; +use oagw_sdk::HTTP_PROTOCOL_ID; +use toolkit_gts::gts_id; +use toolkit_gts::gts_uri; use uuid::Uuid; // 7.8: POST upstream with valid body -> 201 + GTS id + alias generated. @@ -19,7 +22,7 @@ async fn create_upstream_success() { "server": { "endpoints": [{"host": "api.openai.com", "port": 443, "scheme": "https"}] }, - "protocol": "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + "protocol": HTTP_PROTOCOL_ID, "enabled": true, "tags": [] })) @@ -28,7 +31,13 @@ async fn create_upstream_success() { let json = resp.json(); let id_str = json["id"].as_str().unwrap(); - assert!(id_str.starts_with("gts.cf.core.oagw.upstream.v1~")); + assert_eq!( + gts::GtsId::try_new(id_str) + .expect("upstream id should be a valid GTS id") + .get_type_id() + .as_deref(), + Some(gts_id!("cf.core.oagw.upstream.v1~")) + ); assert_eq!(json["alias"].as_str().unwrap(), "api.openai.com"); } @@ -40,7 +49,7 @@ async fn create_upstream_missing_server_returns_422() { h.api_v1() .post_upstream() .with_body(serde_json::json!({ - "protocol": "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1" + "protocol": HTTP_PROTOCOL_ID })) .expect_status(422) .await; @@ -63,7 +72,7 @@ async fn get_upstream_by_gts_id() { port: 443, }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .build(), ) @@ -90,7 +99,7 @@ async fn get_upstream_invalid_gts_returns_400() { let json = resp.json(); assert_eq!( json["type"].as_str().unwrap(), - "gts://gts.cf.core.errors.err.v1~cf.core.err.invalid_argument.v1~" + gts_uri!("cf.core.errors.err.v1~cf.core.err.invalid_argument.v1~") ); } @@ -120,7 +129,7 @@ async fn update_upstream_preserves_id() { port: 443, }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .alias("openai") .build(), @@ -137,7 +146,7 @@ async fn update_upstream_preserves_id() { "server": { "endpoints": [{"host": "10.0.0.1", "port": 443, "scheme": "https"}] }, - "protocol": "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + "protocol": HTTP_PROTOCOL_ID, "alias": "openai", "enabled": true, "tags": [] @@ -167,7 +176,7 @@ async fn delete_upstream_returns_204() { port: 443, }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .build(), ) @@ -196,7 +205,7 @@ async fn create_route_success() { port: 443, }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .build(), ) @@ -222,11 +231,12 @@ async fn create_route_success() { .await; let json = resp.json(); - assert!( - json["id"] - .as_str() - .unwrap() - .starts_with("gts.cf.core.oagw.route.v1~") + assert_eq!( + gts::GtsId::try_new(json["id"].as_str().unwrap()) + .expect("route id should be a valid GTS id") + .get_type_id() + .as_deref(), + Some(gts_id!("cf.core.oagw.route.v1~")) ); } @@ -248,7 +258,7 @@ async fn list_upstreams_with_pagination() { port: 443, }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .build(), ) @@ -297,7 +307,7 @@ async fn get_route_by_gts_id() { "server": { "endpoints": [{"host": "api.openai.com", "port": 443, "scheme": "https"}] }, - "protocol": "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + "protocol": HTTP_PROTOCOL_ID, "enabled": true, "tags": [] })) @@ -352,7 +362,7 @@ async fn update_route_changes_fields() { "server": { "endpoints": [{"host": "api.openai.com", "port": 443, "scheme": "https"}] }, - "protocol": "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + "protocol": HTTP_PROTOCOL_ID, "enabled": true, "tags": [] })) @@ -417,7 +427,7 @@ async fn delete_route_returns_204_then_get_returns_404() { "server": { "endpoints": [{"host": "api.openai.com", "port": 443, "scheme": "https"}] }, - "protocol": "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + "protocol": HTTP_PROTOCOL_ID, "enabled": true, "tags": [] })) @@ -469,7 +479,7 @@ async fn list_routes_filters_by_upstream() { "server": { "endpoints": [{"host": host, "port": 443, "scheme": "https"}] }, - "protocol": "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + "protocol": HTTP_PROTOCOL_ID, "enabled": true, "tags": [] })) diff --git a/gears/system/oagw/oagw/tests/proxy_integration.rs b/gears/system/oagw/oagw/tests/proxy_integration.rs index 5c3d5dd62..ae13c4168 100644 --- a/gears/system/oagw/oagw/tests/proxy_integration.rs +++ b/gears/system/oagw/oagw/tests/proxy_integration.rs @@ -5,14 +5,15 @@ #![cfg(not(feature = "fips"))] use std::collections::HashMap; +use toolkit_gts::{GTS_ID_URI_PREFIX, gts_id}; use http::{Method, StatusCode}; use oagw::test_support::{ APIKEY_AUTH_PLUGIN_ID, AppHarness, MockBody, MockGuard, MockResponse, MockUpstream, OAUTH2_CLIENT_CRED_AUTH_PLUGIN_ID, }; -use oagw_sdk::Body; use oagw_sdk::api::ErrorSource; +use oagw_sdk::{Body, HTTP_PROTOCOL_ID}; use oagw_sdk::{ BurstConfig, CorsConfig, CorsHttpMethod, CreateRouteRequest, CreateUpstreamRequest, Endpoint, HeadersConfig, HttpMatch, HttpMethod, MatchRules, PassthroughMode, PathSuffixMode, @@ -37,7 +38,7 @@ async fn setup_openai_mock() -> AppHarness { "server": { "endpoints": [{"host": "127.0.0.1", "port": h.mock_port(), "scheme": "http"}] }, - "protocol": "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + "protocol": HTTP_PROTOCOL_ID, "alias": "mock-upstream", "enabled": true, "tags": [], @@ -143,7 +144,7 @@ async fn proxy_injects_auth_header() { port: h.mock_port(), }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .alias("auth-hdr-test") .auth(oagw_sdk::AuthConfig { @@ -244,7 +245,7 @@ async fn proxy_sse_streaming() { port: h.mock_port(), }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .alias("sse-test") .build(), @@ -357,7 +358,7 @@ async fn proxy_disabled_upstream_returns_503() { port: 9999, }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .alias("disabled-upstream") .enabled(false) @@ -395,7 +396,7 @@ async fn proxy_rate_limit_exceeded_returns_429() { port: h.mock_port(), }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .alias("rate-limited") .rate_limit(RateLimitConfig { @@ -479,7 +480,7 @@ async fn proxy_rate_limit_scope_user_isolates_by_subject() { port: h.mock_port(), }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .alias("scope-user") .rate_limit(RateLimitConfig { @@ -611,7 +612,7 @@ async fn proxy_rate_limit_scope_route_isolates_by_route() { port: h.mock_port(), }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .alias("scope-route") .build(), @@ -774,7 +775,7 @@ async fn proxy_upstream_timeout_returns_504() { port: h.mock_port(), }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .alias("timeout-upstream") .build(), @@ -831,7 +832,7 @@ async fn proxy_query_allowlist_allowed_param_succeeds() { port: h.mock_port(), }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .alias("ql-test") .build(), @@ -885,7 +886,7 @@ async fn proxy_query_allowlist_unknown_param_rejected() { port: h.mock_port(), }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .alias("ql-reject") .build(), @@ -942,11 +943,12 @@ async fn proxy_nonexistent_auth_plugin_returns_error() { port: h.mock_port(), }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .alias("bad-auth") .auth(oagw_sdk::AuthConfig { - plugin_type: "gts.cf.core.oagw.auth.v1~nonexistent.plugin.v1".into(), + plugin_type: gts_id!("cf.core.oagw.auth_plugin.v1~cf.core.oagw.nonexistent.v1") + .into(), sharing: SharingMode::Private, config: None, }) @@ -1019,7 +1021,7 @@ async fn proxy_recorded_request_has_correct_uri_and_body() { port: h.mock_port(), }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .alias("rec-test") .build(), @@ -1085,7 +1087,7 @@ async fn proxy_response_headers_sanitized() { port: h.mock_port(), }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .alias("resp-hdr-test") .build(), @@ -1177,7 +1179,7 @@ async fn proxy_path_suffix_disabled_rejects_extra_path() { port: h.mock_port(), }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .alias("psm-test") .build(), @@ -1265,7 +1267,7 @@ async fn proxy_multi_endpoint_round_robin() { }, ], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .alias("rr-test") .build(), @@ -1345,7 +1347,7 @@ async fn proxy_target_host_header_selects_endpoint() { }, ], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .alias("target-host-test") .build(), @@ -1402,7 +1404,7 @@ async fn proxy_target_host_unknown_returns_error() { port: h.mock_port(), }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .alias("target-host-unknown") .build(), @@ -1468,7 +1470,7 @@ async fn proxy_all_backends_unreachable() { port: 19991, }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .alias("unreachable-test") .build(), @@ -1538,7 +1540,7 @@ async fn proxy_crud_invalidation_after_update() { "server": { "endpoints": [{"host": "127.0.0.1", "port": port_a, "scheme": "http"}] }, - "protocol": "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + "protocol": HTTP_PROTOCOL_ID, "alias": "crud-invalidation", "enabled": true, "tags": [] @@ -1590,7 +1592,7 @@ async fn proxy_crud_invalidation_after_update() { "server": { "endpoints": [{"host": "127.0.0.1", "port": port_b, "scheme": "http"}] }, - "protocol": "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + "protocol": HTTP_PROTOCOL_ID, "alias": "crud-invalidation", "enabled": true, "tags": [] @@ -1648,7 +1650,7 @@ async fn proxy_with_mock_guard_custom_response() { port: h.mock_port(), }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .alias("guard-test") .build(), @@ -1717,7 +1719,7 @@ async fn proxy_websocket_upgrade_returns_101() { port: h.mock_port(), }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .alias("ws-echo-test") .build(), @@ -1795,7 +1797,7 @@ async fn proxy_websocket_upgrade_rejected_returns_503_protocol_error() { port: h.mock_port(), }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .alias("ws-reject-test") .build(), @@ -1907,7 +1909,7 @@ async fn proxy_websocket_auth_injected_during_handshake() { port: h.mock_port(), }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .alias("ws-auth-test") .auth(oagw_sdk::AuthConfig { @@ -2019,7 +2021,7 @@ async fn proxy_websocket_rate_limit_on_handshake() { port: h.mock_port(), }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .alias("ws-rate-limit") .rate_limit(RateLimitConfig { @@ -2157,7 +2159,7 @@ async fn proxy_unreachable_backend_returns_rfc9457_problem_body() { port: 19993, }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .alias("rfc9457-body-test") .build(), @@ -2226,8 +2228,11 @@ async fn proxy_unreachable_backend_returns_rfc9457_problem_body() { // GTS type must not be about:blank. let gts_type = body["type"].as_str().unwrap(); + let gts_id = gts_type + .strip_prefix(GTS_ID_URI_PREFIX) + .expect("canonical problem type should use GTS URI prefix"); assert!( - gts_type.starts_with("gts://gts."), + gts::GtsId::try_new(gts_id).is_ok(), "expected canonical GTS error type, got: {gts_type}" ); @@ -2273,7 +2278,7 @@ async fn proxy_streaming_body_exceeding_limit_returns_400() { port: h.mock_port(), }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .alias("body-limit-test") .build(), @@ -2358,7 +2363,7 @@ async fn proxy_streaming_body_post_arrives_intact() { port: h.mock_port(), }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .alias("stream-body-test") .build(), @@ -2449,7 +2454,7 @@ async fn proxy_streaming_body_with_empty_chunks_succeeds() { port: h.mock_port(), }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .alias("stream-empty-chunks") .build(), @@ -2543,7 +2548,7 @@ async fn proxy_streaming_body_single_chunk() { port: h.mock_port(), }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .alias("stream-single-chunk") .build(), @@ -2629,7 +2634,7 @@ async fn proxy_streaming_body_error_mid_stream_does_not_send_terminator() { port: h.mock_port(), }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .alias("stream-err-test") .build(), @@ -2738,7 +2743,7 @@ async fn proxy_oauth2_client_cred_injects_bearer_token() { port: h.mock_port(), }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .alias("oauth2-test") .auth(oagw_sdk::AuthConfig { @@ -2843,7 +2848,7 @@ async fn proxy_oauth2_missing_credentials_returns_error() { port: h.mock_port(), }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .alias("oauth2-missing-creds") .auth(oagw_sdk::AuthConfig { @@ -2910,7 +2915,7 @@ async fn proxy_oauth2_missing_credentials_returns_error() { // --------------------------------------------------------------------------- const REQUIRED_HEADERS_GUARD_PLUGIN_ID: &str = - "gts.cf.core.oagw.guard_plugin.v1~cf.core.oagw.required_headers.v1"; + gts_id!("cf.core.oagw.guard_plugin.v1~cf.core.oagw.required_headers.v1"); /// Verify that the RequiredHeadersGuardPlugin allows requests that include /// all required headers. @@ -2942,7 +2947,7 @@ async fn proxy_guard_allows_when_required_header_present() { port: h.mock_port(), }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .alias("guard-hdr-ok") .headers(HeadersConfig { @@ -3026,7 +3031,7 @@ async fn proxy_guard_rejects_missing_required_header() { port: h.mock_port(), }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .alias("guard-hdr-miss") .plugins(PluginsConfig { @@ -3135,7 +3140,7 @@ async fn proxy_guard_allows_unconfigured() { port: h.mock_port(), }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .alias("guard-hdr-noconf") .plugins(PluginsConfig { @@ -3189,7 +3194,7 @@ async fn proxy_guard_allows_unconfigured() { // --------------------------------------------------------------------------- const REQUEST_ID_TRANSFORM_PLUGIN_ID: &str = - "gts.cf.core.oagw.transform_plugin.v1~cf.core.oagw.request_id.v1"; + gts_id!("cf.core.oagw.transform_plugin.v1~cf.core.oagw.request_id.v1"); /// Verify that the RequestIdTransformPlugin injects an X-Request-ID header when /// the inbound request does not include one. @@ -3221,7 +3226,7 @@ async fn proxy_transform_injects_request_id() { port: h.mock_port(), }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .alias("transform-inject") .plugins(PluginsConfig { @@ -3313,7 +3318,7 @@ async fn proxy_transform_preserves_request_id() { port: h.mock_port(), }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .alias("transform-preserve") .headers(HeadersConfig { @@ -3416,14 +3421,16 @@ async fn proxy_transform_error_continues_pipeline() { port: h.mock_port(), }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .alias("transform-error") .plugins(PluginsConfig { sharing: SharingMode::Private, items: vec![PluginBinding { - plugin_ref: "gts.cf.core.oagw.transform_plugin.v1~cf.core.oagw.nonexistent.v1" - .to_string(), + plugin_ref: gts_id!( + "cf.core.oagw.transform_plugin.v1~cf.core.oagw.nonexistent.v1" + ) + .to_string(), config: Default::default(), }], }) @@ -3503,7 +3510,7 @@ async fn setup_cors_upstream( port: h.mock_port(), }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .alias(alias); @@ -3837,7 +3844,7 @@ async fn cors_route_inherit_merges_origins() { port: h.mock_port(), }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .alias("cors-route-inherit"); builder = builder.cors(upstream_cors); @@ -3964,7 +3971,7 @@ async fn cors_actual_request_disallowed_method_rejected_before_upstream() { port: h.mock_port(), }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .alias("cors-reject-method"); builder = builder.cors(cors); @@ -4312,7 +4319,7 @@ async fn setup_ws_upstream(h: &AppHarness, alias: &str) { port: h.mock_port(), }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .alias(alias) .build(), @@ -4786,7 +4793,7 @@ async fn proxy_response_header_rules_applied() { port: h.mock_port(), }], }, - "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1", + HTTP_PROTOCOL_ID, ) .alias("resp-rules-test") .headers(HeadersConfig { diff --git a/gears/system/resource-group/docs/features/0001-sdk-gear-foundation.md b/gears/system/resource-group/docs/features/0001-sdk-gear-foundation.md index 48f71081e..ace9ed285 100644 --- a/gears/system/resource-group/docs/features/0001-sdk-gear-foundation.md +++ b/gears/system/resource-group/docs/features/0001-sdk-gear-foundation.md @@ -97,10 +97,8 @@ Not applicable. This feature provides SDK contracts and gear infrastructure with 1. [x] - `p1` - **RETURN** Validation error: "GTS type path must not be empty" - `inst-gts-val-3a` 4. [x] - `p1` - **IF** string does not match pattern `^gts\.[a-z0-9_.]+~([a-z0-9_.]+~)*$` - `inst-gts-val-4` 1. [x] - `p1` - **RETURN** Validation error: "Invalid GTS type path format" - `inst-gts-val-4a` -5. [x] - `p1` - **IF** string length exceeds maximum (255 chars) - `inst-gts-val-5` - 1. [x] - `p1` - **RETURN** Validation error: "GTS type path exceeds maximum length" - `inst-gts-val-5a` -6. [x] - `p1` - Construct `GtsTypePath` value object wrapping the validated string - `inst-gts-val-6` -7. [x] - `p1` - **RETURN** validated `GtsTypePath` - `inst-gts-val-7` +5. [x] - `p1` - Construct `GtsTypePath` value object wrapping the validated string - `inst-gts-val-6` +6. [x] - `p1` - **RETURN** validated `GtsTypePath` - `inst-gts-val-7` ### Domain Error to Problem Mapping diff --git a/gears/system/resource-group/resource-group-sdk/src/api.rs b/gears/system/resource-group/resource-group-sdk/src/api.rs index 16ad11f08..c65c4c607 100644 --- a/gears/system/resource-group/resource-group-sdk/src/api.rs +++ b/gears/system/resource-group/resource-group-sdk/src/api.rs @@ -20,7 +20,7 @@ use crate::models::{ /// Consumers obtain this from `ClientHub`: /// ```ignore /// let client = hub.get::()?; -/// let rg_type = client.get_type(&ctx, "gts.cf.core.rg.type.v1~...").await?; +/// let rg_type = client.get_type(&ctx, tenant_resource_group_type).await?; /// ``` /// /// # Error envelope diff --git a/gears/system/resource-group/resource-group-sdk/src/error_tests.rs b/gears/system/resource-group/resource-group-sdk/src/error_tests.rs index 9bebcddb7..e7c81908b 100644 --- a/gears/system/resource-group/resource-group-sdk/src/error_tests.rs +++ b/gears/system/resource-group/resource-group-sdk/src/error_tests.rs @@ -25,7 +25,7 @@ mod wire_vocabulary_round_trip { // Its literal MUST equal `gts::GROUP_RESOURCE_TYPE` — the // `gts_resource_type_round_trip` test asserts that equality (the // proc-macro cannot reference the const directly). - #[resource_error("gts.cf.core.resource_group.group.v1~")] + #[resource_error(gts_id!("cf.core.resource_group.group.v1~"))] struct RgScope; fn problem(err: CanonicalError) -> serde_json::Value { @@ -132,7 +132,7 @@ mod projection_tests { use crate::{field, gts, precondition, reason}; use toolkit_canonical_errors::{CanonicalError, Problem, resource_error}; - #[resource_error("gts.cf.core.resource_group.group.v1~")] + #[resource_error(gts_id!("cf.core.resource_group.group.v1~"))] struct RgScope; #[test] diff --git a/gears/system/resource-group/resource-group-sdk/src/gts.rs b/gears/system/resource-group/resource-group-sdk/src/gts.rs index 5fb53b5dd..b5d771ce4 100644 --- a/gears/system/resource-group/resource-group-sdk/src/gts.rs +++ b/gears/system/resource-group/resource-group-sdk/src/gts.rs @@ -1,6 +1,7 @@ // Created: 2026-04-16 by Constructor Tech //! GTS schema definitions for the Resource Group type system. +use toolkit_gts::gts_id; use toolkit_gts::gts_type_schema; /// GTS base type schema for Resource Group types. @@ -35,7 +36,7 @@ use toolkit_gts::gts_type_schema; /// ``` #[gts_type_schema( dir_path = "schemas", - type_id = "gts.cf.core.rg.type.v1~", + type_id = gts_id!("cf.core.rg.type.v1~"), description = "Resource Group base type — defines placement and tenant scope traits", properties = "id,can_be_root,allowed_parent_types,allowed_membership_types", base = true @@ -62,7 +63,10 @@ pub struct ResourceGroupTypeV1 { /// Match it against the resource-scoped projection variants /// ([`crate::ResourceGroupError::NotFound`] / /// [`crate::ResourceGroupError::AlreadyExists`]). -pub const GROUP_RESOURCE_TYPE: &str = "gts.cf.core.resource_group.group.v1~"; +pub const GROUP_RESOURCE_TYPE: &str = gts_id!("cf.core.resource_group.group.v1~"); + +/// Canonical GTS resource type for a resource-group membership link. +pub const GROUP_MEMBERSHIP_RESOURCE_TYPE: &str = gts_id!("cf.core.rg.group_membership.v1~"); /// GTS type path for the tenant resource-group type. /// @@ -74,4 +78,4 @@ pub const GROUP_RESOURCE_TYPE: &str = "gts.cf.core.resource_group.group.v1~"; /// /// The tenant RG type itself is seeded externally (via API/config) with /// `can_be_root: true` so root tenants are valid placements. -pub const TENANT_RG_TYPE_PATH: &str = "gts.cf.core.rg.type.v1~cf.core._.tenant.v1~"; +pub const TENANT_RG_TYPE_PATH: &str = gts_id!("cf.core.rg.type.v1~cf.core._.tenant.v1~"); diff --git a/gears/system/resource-group/resource-group-sdk/src/lib.rs b/gears/system/resource-group/resource-group-sdk/src/lib.rs index 7a83e346f..43b601f1e 100644 --- a/gears/system/resource-group/resource-group-sdk/src/lib.rs +++ b/gears/system/resource-group/resource-group-sdk/src/lib.rs @@ -32,7 +32,7 @@ pub mod odata; // Re-export main types at crate root for convenience pub use api::{ResourceGroupClient, ResourceGroupReadHierarchy}; pub use error::ResourceGroupError; -pub use gts::{GROUP_RESOURCE_TYPE, TENANT_RG_TYPE_PATH}; +pub use gts::{GROUP_MEMBERSHIP_RESOURCE_TYPE, GROUP_RESOURCE_TYPE, TENANT_RG_TYPE_PATH}; pub use models::{ CreateGroupRequest, CreateTypeRequest, GroupHierarchy, GroupHierarchyWithDepth, GtsTypePath, ResourceGroup, ResourceGroupMembership, ResourceGroupType, ResourceGroupWithDepth, diff --git a/gears/system/resource-group/resource-group-sdk/src/models.rs b/gears/system/resource-group/resource-group-sdk/src/models.rs index 38106ab8f..27e048002 100644 --- a/gears/system/resource-group/resource-group-sdk/src/models.rs +++ b/gears/system/resource-group/resource-group-sdk/src/models.rs @@ -16,9 +16,6 @@ use uuid::Uuid; // -- GtsTypePath value object -- // @cpt-begin:cpt-cf-resource-group-algo-sdk-foundation-validate-gts-type-path:p1:inst-gts-val-1 -/// Maximum length of a GTS type path. -const GTS_TYPE_PATH_MAX_LEN: usize = 1024; - /// Validated GTS type path value object. /// /// A GTS type path follows the pattern `gts.~(~)*` where @@ -32,8 +29,8 @@ impl GtsTypePath { /// Create a new `GtsTypePath` from a raw string, applying validation. /// /// # Errors - /// Returns an error if the string is empty, exceeds 1024 characters, - /// or does not match the GTS type path format. + /// Returns an error if the string is empty or does not match the GTS + /// type path format (including exceeding the 1024-char GTS ID limit). pub fn new(raw: impl Into) -> Result { // @cpt-begin:cpt-cf-resource-group-algo-sdk-foundation-validate-gts-type-path:p1:inst-gts-val-2 let raw = raw.into(); @@ -48,19 +45,11 @@ impl GtsTypePath { } // @cpt-end:cpt-cf-resource-group-algo-sdk-foundation-validate-gts-type-path:p1:inst-gts-val-3 - // @cpt-begin:cpt-cf-resource-group-algo-sdk-foundation-validate-gts-type-path:p1:inst-gts-val-5 - if s.len() > GTS_TYPE_PATH_MAX_LEN { - // @cpt-begin:cpt-cf-resource-group-algo-sdk-foundation-validate-gts-type-path:p1:inst-gts-val-5a - return Err("GTS type path exceeds maximum length".to_owned()); - // @cpt-end:cpt-cf-resource-group-algo-sdk-foundation-validate-gts-type-path:p1:inst-gts-val-5a - } - // @cpt-end:cpt-cf-resource-group-algo-sdk-foundation-validate-gts-type-path:p1:inst-gts-val-5 - // @cpt-begin:cpt-cf-resource-group-algo-sdk-foundation-validate-gts-type-path:p1:inst-gts-val-4 // Validate format using the canonical gts crate parser. // Each tilde-separated segment must be a valid GTS ID with 5+ tokens // (vendor.package.namespace.type.vMAJOR). - if gts::GtsID::new(&s).is_err() { + if gts::GtsId::try_new(&s).is_err() { // @cpt-begin:cpt-cf-resource-group-algo-sdk-foundation-validate-gts-type-path:p1:inst-gts-val-4a return Err("Invalid GTS type path format".to_owned()); // @cpt-end:cpt-cf-resource-group-algo-sdk-foundation-validate-gts-type-path:p1:inst-gts-val-4a diff --git a/gears/system/resource-group/resource-group-sdk/src/models_tests.rs b/gears/system/resource-group/resource-group-sdk/src/models_tests.rs index 4451a78f9..b15c6e253 100644 --- a/gears/system/resource-group/resource-group-sdk/src/models_tests.rs +++ b/gears/system/resource-group/resource-group-sdk/src/models_tests.rs @@ -1,5 +1,6 @@ // Created: 2026-04-16 by Constructor Tech use super::*; +use toolkit_gts::{GTS_ID_PREFIX, gts_id}; // -- GtsTypePath: valid cases (table-driven) -- TC-SDK-01, 06, 07, 08, 19, 20 @@ -7,20 +8,32 @@ use super::*; fn gts_type_path_valid_cases() { let valid = vec![ // TC-SDK-01: basic valid path - ("gts.cf.core.rg.type.v1~", "gts.cf.core.rg.type.v1~"), + ( + gts_id!("cf.core.rg.type.v1~"), + gts_id!("cf.core.rg.type.v1~"), + ), // TC-SDK-06: uppercase is lowered - ("GTS.CF.CORE.RG.TYPE.V1~", "gts.cf.core.rg.type.v1~"), + ("GTS.CF.CORE.RG.TYPE.V1~", gts_id!("cf.core.rg.type.v1~")), // TC-SDK-07: trimmed + lowered - (" GTS.CF.CORE.RG.TYPE.V1~ ", "gts.cf.core.rg.type.v1~"), + ( + " GTS.CF.CORE.RG.TYPE.V1~ ", + gts_id!("cf.core.rg.type.v1~"), + ), // TC-SDK-08: multi-segment ( - "gts.cf.core.rg.type.v1~x.test.unit.root.v1~", - "gts.cf.core.rg.type.v1~x.test.unit.root.v1~", + gts_id!("cf.core.rg.type.v1~x.test.unit.root.v1~"), + gts_id!("cf.core.rg.type.v1~x.test.unit.root.v1~"), ), // TC-SDK-19: numeric version in segment - ("gts.cf.core.rg.type.v2~", "gts.cf.core.rg.type.v2~"), + ( + gts_id!("cf.core.rg.type.v2~"), + gts_id!("cf.core.rg.type.v2~"), + ), // TC-SDK-20: underscores in segments - ("gts.cf.core.rg.my_type.v1~", "gts.cf.core.rg.my_type.v1~"), + ( + gts_id!("cf.core.rg.my_type.v1~"), + gts_id!("cf.core.rg.my_type.v1~"), + ), ]; for (input, expected) in valid { let path = GtsTypePath::new(input); @@ -35,25 +48,31 @@ fn gts_type_path_valid_cases() { fn gts_type_path_invalid_cases() { let cases = vec![ // TC-SDK-02: empty - ("", "must not be empty"), + (String::new(), "must not be empty"), // TC-SDK-04: wrong prefix - ("invalid.path~", "Invalid GTS type path format"), + ("invalid.path~".to_owned(), "Invalid GTS type path format"), // TC-SDK-05: no trailing tilde - ("gts.cf.core.rg.type.v1", "Invalid GTS type path format"), + ( + format!("{GTS_ID_PREFIX}cf.core.rg.type.v1"), + "Invalid GTS type path format", + ), // TC-SDK-09: double tilde - ("gts.cf.core.rg.type.v1~~", "Invalid GTS type path format"), + ( + format!("{GTS_ID_PREFIX}cf.core.rg.type.v1~~"), + "Invalid GTS type path format", + ), // TC-SDK-10: hyphen in segment ( - "gts.cf.core.rg.type.v1~hello-world~", + format!("{GTS_ID_PREFIX}cf.core.rg.type.v1~hello-world~"), "Invalid GTS type path format", ), // TC-SDK-18: empty segment after gts. - ("gts.~", "Invalid GTS type path format"), + (format!("{GTS_ID_PREFIX}~"), "Invalid GTS type path format"), // TC-SDK-21: whitespace-only - (" ", "must not be empty"), + (" ".to_owned(), "must not be empty"), ]; for (input, expected_msg) in cases { - let result = GtsTypePath::new(input); + let result = GtsTypePath::new(input.as_str()); assert!(result.is_err(), "should be invalid: '{input}'"); let err = result.unwrap_err(); assert!( @@ -69,12 +88,12 @@ fn gts_type_path_invalid_cases() { #[allow(unknown_lints, de0901_gts_string_pattern)] fn gts_type_path_max_length_boundary() { // TC-SDK-22: exactly 1024 chars -> Ok - // Base: "gts.cf.core.rg.type.v1~" = 24 chars. + // Base length includes the configured GTS prefix. // Pad the type name to fill remaining: 1024 - 24 = 1000 chars in first segment. - // "gts.cf.core.rg." (15) + padded_name + ".v1~" (4) = 1024 + // configured prefix + "cf.core.rg." + padded_name + ".v1~" = 1024 // padded_name = 1024 - 15 - 4 = 1005 chars let name = "a".repeat(1005); - let path_1024 = format!("gts.cf.core.rg.{name}.v1~"); + let path_1024 = format!("{GTS_ID_PREFIX}cf.core.rg.{name}.v1~"); assert_eq!(path_1024.len(), 1024); assert!( GtsTypePath::new(&path_1024).is_ok(), @@ -84,11 +103,11 @@ fn gts_type_path_max_length_boundary() { // TC-SDK-23: > 1024 chars -> Err (exceeds max length) let name_long = "a".repeat(1006); - let path_1025 = format!("gts.cf.core.rg.{name_long}.v1~"); + let path_1025 = format!("{GTS_ID_PREFIX}cf.core.rg.{name_long}.v1~"); assert_eq!(path_1025.len(), 1025); let result = GtsTypePath::new(&path_1025); assert!(result.is_err(), "1025 chars should exceed max length"); - assert!(result.unwrap_err().contains("exceeds maximum length")); + assert!(result.unwrap_err().contains("Invalid GTS type path format")); } // -- GtsTypePath: serde round-trip -- TC-SDK-11, 12 @@ -96,7 +115,7 @@ fn gts_type_path_max_length_boundary() { #[test] fn gts_type_path_serde_round_trip() { // TC-SDK-11: serialize then deserialize - let original = GtsTypePath::new("gts.cf.core.rg.type.v1~").unwrap(); + let original = GtsTypePath::new(gts_id!("cf.core.rg.type.v1~")).unwrap(); let json = serde_json::to_string(&original).unwrap(); let deserialized: GtsTypePath = serde_json::from_str(&json).unwrap(); assert_eq!(original, deserialized); @@ -113,7 +132,7 @@ fn gts_type_path_serde_invalid_rejects() { #[test] fn gts_type_path_display_and_into_string() { - let path = GtsTypePath::new("gts.cf.core.rg.type.v1~").unwrap(); + let path = GtsTypePath::new(gts_id!("cf.core.rg.type.v1~")).unwrap(); let display = path.to_string(); let into_string: String = path.into(); assert_eq!(display, into_string); @@ -124,10 +143,10 @@ fn gts_type_path_display_and_into_string() { #[test] fn resource_group_type_camel_case_keys() { let rgt = ResourceGroupType { - code: "gts.cf.core.rg.type.v1~".to_owned(), + code: gts_id!("cf.core.rg.type.v1~").to_owned(), can_be_root: true, - allowed_parent_types: vec!["gts.parent~".to_owned()], - allowed_membership_types: vec!["gts.member~".to_owned()], + allowed_parent_types: vec![format!("{GTS_ID_PREFIX}parent~")], + allowed_membership_types: vec![format!("{GTS_ID_PREFIX}member~")], metadata_schema: None, }; let json = serde_json::to_value(&rgt).unwrap(); @@ -156,7 +175,7 @@ fn resource_group_type_field_renamed() { // TC-SDK-15: Rust field is `code`, serialized as JSON key `"type"`. let group = ResourceGroup { id: Uuid::nil(), - code: "gts.cf.core.rg.type.v1~".to_owned(), + code: gts_id!("cf.core.rg.type.v1~").to_owned(), name: "Test".to_owned(), hierarchy: GroupHierarchy { parent_id: None, @@ -180,7 +199,7 @@ fn resource_group_metadata_absent_when_none() { // TC-SDK-16: metadata: None -> no "metadata" key let group = ResourceGroup { id: Uuid::nil(), - code: "gts.cf.core.rg.type.v1~".to_owned(), + code: gts_id!("cf.core.rg.type.v1~").to_owned(), name: "Test".to_owned(), hierarchy: GroupHierarchy { parent_id: None, @@ -208,5 +227,5 @@ fn gts_type_path_trims_and_lowercases() { path.is_ok(), "GtsTypePath::new should accept trimmed/lowered input" ); - assert_eq!(path.unwrap().as_str(), "gts.cf.core.rg.type.v1~"); + assert_eq!(path.unwrap().as_str(), gts_id!("cf.core.rg.type.v1~")); } diff --git a/gears/system/resource-group/resource-group/Cargo.toml b/gears/system/resource-group/resource-group/Cargo.toml index 2705c51de..7dc320e43 100644 --- a/gears/system/resource-group/resource-group/Cargo.toml +++ b/gears/system/resource-group/resource-group/Cargo.toml @@ -63,6 +63,7 @@ gts = { workspace = true } toolkit = { workspace = true } toolkit-canonical-errors = { workspace = true } toolkit-db = { workspace = true, features = ["sqlite", "pg"] } +toolkit-gts = { workspace = true } toolkit-db-macros = { workspace = true } toolkit-security = { workspace = true } toolkit-odata = { workspace = true, features = ["with-utoipa"] } diff --git a/gears/system/resource-group/resource-group/src/api/rest/dto_tests.rs b/gears/system/resource-group/resource-group/src/api/rest/dto_tests.rs index 65b557140..7fb98fff3 100644 --- a/gears/system/resource-group/resource-group/src/api/rest/dto_tests.rs +++ b/gears/system/resource-group/resource-group/src/api/rest/dto_tests.rs @@ -1,23 +1,33 @@ // Created: 2026-04-16 by Constructor Tech // @cpt-dod:cpt-cf-resource-group-dod-testing-rest-api:p2 use super::*; +use toolkit_gts::{GTS_ID_PREFIX, gts_id}; use uuid::Uuid; // TC-DTO-01: ResourceGroupType -> TypeDto #[test] fn dto_type_from_resource_group_type() { let rgt = ResourceGroupType { - code: "gts.cf.system.rg.type.v1~x.test.dto.mytype.v1~".to_owned(), + code: gts_id!("cf.system.rg.type.v1~x.test.dto.mytype.v1~").to_owned(), can_be_root: true, - allowed_parent_types: vec!["gts.parent~".to_owned()], - allowed_membership_types: vec!["gts.member~".to_owned()], + allowed_parent_types: vec![format!("{GTS_ID_PREFIX}parent~")], + allowed_membership_types: vec![format!("{GTS_ID_PREFIX}member~")], metadata_schema: Some(serde_json::json!({"type": "object"})), }; let dto: TypeDto = rgt.into(); - assert_eq!(dto.code, "gts.cf.system.rg.type.v1~x.test.dto.mytype.v1~"); + assert_eq!( + dto.code, + gts_id!("cf.system.rg.type.v1~x.test.dto.mytype.v1~") + ); assert!(dto.can_be_root); - assert_eq!(dto.allowed_parent_types, vec!["gts.parent~"]); - assert_eq!(dto.allowed_membership_types, vec!["gts.member~"]); + assert_eq!( + dto.allowed_parent_types, + vec![format!("{GTS_ID_PREFIX}parent~")] + ); + assert_eq!( + dto.allowed_membership_types, + vec![format!("{GTS_ID_PREFIX}member~")] + ); assert!(dto.metadata_schema.is_some()); } @@ -25,16 +35,22 @@ fn dto_type_from_resource_group_type() { #[test] fn dto_create_type_to_request() { let dto = CreateTypeDto { - code: "gts.cf.system.rg.type.v1~x.test.dto.mytype.v1~".to_owned(), + code: gts_id!("cf.system.rg.type.v1~x.test.dto.mytype.v1~").to_owned(), can_be_root: false, - allowed_parent_types: vec!["gts.parent~".to_owned()], + allowed_parent_types: vec![format!("{GTS_ID_PREFIX}parent~")], allowed_membership_types: vec![], metadata_schema: None, }; let req: CreateTypeRequest = dto.into(); - assert_eq!(req.code, "gts.cf.system.rg.type.v1~x.test.dto.mytype.v1~"); + assert_eq!( + req.code, + gts_id!("cf.system.rg.type.v1~x.test.dto.mytype.v1~") + ); assert!(!req.can_be_root); - assert_eq!(req.allowed_parent_types, vec!["gts.parent~"]); + assert_eq!( + req.allowed_parent_types, + vec![format!("{GTS_ID_PREFIX}parent~")] + ); assert!(req.allowed_membership_types.is_empty()); assert!(req.metadata_schema.is_none()); } @@ -46,7 +62,7 @@ fn dto_group_from_resource_group() { let tenant_id = Uuid::now_v7(); let group = ResourceGroup { id: Uuid::now_v7(), - code: "gts.cf.system.rg.type.v1~".to_owned(), + code: gts_id!("cf.system.rg.type.v1~").to_owned(), name: "My Group".to_owned(), hierarchy: resource_group_sdk::models::GroupHierarchy { parent_id: Some(parent_id), @@ -56,7 +72,7 @@ fn dto_group_from_resource_group() { }; let dto: GroupDto = group.clone().into(); assert_eq!(dto.id, group.id); - assert_eq!(dto.type_path, "gts.cf.system.rg.type.v1~"); + assert_eq!(dto.type_path, gts_id!("cf.system.rg.type.v1~")); assert_eq!(dto.name, "My Group"); assert_eq!(dto.hierarchy.parent_id, Some(parent_id)); assert_eq!(dto.hierarchy.tenant_id, tenant_id); @@ -69,7 +85,7 @@ fn dto_group_with_depth_from_resource_group() { let tenant_id = Uuid::now_v7(); let gwd = ResourceGroupWithDepth { id: Uuid::now_v7(), - code: "gts.cf.system.rg.type.v1~".to_owned(), + code: gts_id!("cf.system.rg.type.v1~").to_owned(), name: "Depth Group".to_owned(), hierarchy: resource_group_sdk::models::GroupHierarchyWithDepth { parent_id: None, @@ -85,14 +101,18 @@ fn dto_group_with_depth_from_resource_group() { assert_eq!(dto.hierarchy.tenant_id, tenant_id); } -// TC-DTO-05: Deserialize {"type": "gts...", "name": "X"} into CreateGroupDto +// TC-DTO-05: Deserialize {"type": gts_id!(".."), "name": "X"} into CreateGroupDto #[test] fn dto_create_group_deserialize_type_key() { - let json = r#"{"type": "gts.cf.system.rg.type.v1~x.test.dto.mytype.v1~", "name": "X"}"#; - let dto: CreateGroupDto = serde_json::from_str(json).unwrap(); + let json = serde_json::json!({ + "type": gts_id!("cf.system.rg.type.v1~x.test.dto.mytype.v1~"), + "name": "X" + }) + .to_string(); + let dto: CreateGroupDto = serde_json::from_str(&json).unwrap(); assert_eq!( dto.type_path, - "gts.cf.system.rg.type.v1~x.test.dto.mytype.v1~" + gts_id!("cf.system.rg.type.v1~x.test.dto.mytype.v1~") ); assert_eq!(dto.name, "X"); assert!(dto.parent_id.is_none()); @@ -101,8 +121,12 @@ fn dto_create_group_deserialize_type_key() { // TC-DTO-06: Deserialize with no vectors -> defaults to empty #[test] fn dto_create_type_deserialize_missing_vectors_default_empty() { - let json = r#"{"code": "gts.cf.system.rg.type.v1~x.test.dto.mytype.v1~", "can_be_root": true}"#; - let dto: CreateTypeDto = serde_json::from_str(json).unwrap(); + let json = serde_json::json!({ + "code": gts_id!("cf.system.rg.type.v1~x.test.dto.mytype.v1~"), + "can_be_root": true + }) + .to_string(); + let dto: CreateTypeDto = serde_json::from_str(&json).unwrap(); assert!(dto.allowed_parent_types.is_empty()); assert!(dto.allowed_membership_types.is_empty()); } @@ -112,7 +136,7 @@ fn dto_create_type_deserialize_missing_vectors_default_empty() { fn dto_membership_serialize_no_tenant_id() { let membership = ResourceGroupMembership { group_id: Uuid::now_v7(), - resource_type: "gts.cf.system.rg.type.v1~".to_owned(), + resource_type: gts_id!("cf.system.rg.type.v1~").to_owned(), resource_id: "res-001".to_owned(), }; let dto: MembershipDto = membership.into(); diff --git a/gears/system/resource-group/resource-group/src/api/rest/error.rs b/gears/system/resource-group/resource-group/src/api/rest/error.rs index 4de5eba36..171eeb814 100644 --- a/gears/system/resource-group/resource-group/src/api/rest/error.rs +++ b/gears/system/resource-group/resource-group/src/api/rest/error.rs @@ -18,7 +18,7 @@ use crate::domain::error::DomainError; /// The macro literal mirrors [`resource_group_sdk::gts::GROUP_RESOURCE_TYPE`] /// (proc-macros cannot resolve a const); the SDK round-trip tests pin the /// two equal. -#[resource_error("gts.cf.core.resource_group.group.v1~")] +#[resource_error(gts_id!("cf.core.resource_group.group.v1~"))] pub struct RgError; /// Implement `From for CanonicalError` so `?` works in diff --git a/gears/system/resource-group/resource-group/src/domain/group_service.rs b/gears/system/resource-group/resource-group/src/domain/group_service.rs index 4beaf6d67..e41efdf1a 100644 --- a/gears/system/resource-group/resource-group/src/domain/group_service.rs +++ b/gears/system/resource-group/resource-group/src/domain/group_service.rs @@ -16,10 +16,10 @@ use std::sync::Arc; use authz_resolver_sdk::pep::{PolicyEnforcer, ResourceType}; -use resource_group_sdk::TENANT_RG_TYPE_PATH; use resource_group_sdk::models::{ CreateGroupRequest, ResourceGroup, ResourceGroupWithDepth, UpdateGroupRequest, }; +use resource_group_sdk::{GROUP_RESOURCE_TYPE, TENANT_RG_TYPE_PATH}; use toolkit_db::secure::{DBRunner, TxConfig}; use toolkit_odata::{ODataQuery, Page}; use toolkit_security::{SecurityContext, pep_properties}; @@ -33,7 +33,7 @@ use crate::domain::validation; /// `AuthZ` resource type descriptor for resource groups. pub const RG_GROUP_RESOURCE: ResourceType = ResourceType::from_static( - "gts.cf.core.rg.group.v1~", + GROUP_RESOURCE_TYPE, &[pep_properties::OWNER_TENANT_ID, pep_properties::RESOURCE_ID], ); diff --git a/gears/system/resource-group/resource-group/src/domain/membership_service.rs b/gears/system/resource-group/resource-group/src/domain/membership_service.rs index df3bd2553..8aeac8c01 100644 --- a/gears/system/resource-group/resource-group/src/domain/membership_service.rs +++ b/gears/system/resource-group/resource-group/src/domain/membership_service.rs @@ -9,7 +9,7 @@ use std::sync::Arc; use authz_resolver_sdk::pep::{PolicyEnforcer, ResourceType}; -use resource_group_sdk::models::ResourceGroupMembership; +use resource_group_sdk::{GROUP_MEMBERSHIP_RESOURCE_TYPE, models::ResourceGroupMembership}; use toolkit_odata::{ODataQuery, Page}; use toolkit_security::{SecurityContext, pep_properties}; use uuid::Uuid; @@ -22,7 +22,7 @@ use crate::domain::repo::{GroupRepositoryTrait, MembershipRepositoryTrait, TypeR /// `AuthZ` resource type descriptor for group memberships. pub const RG_MEMBERSHIP_RESOURCE: ResourceType = ResourceType::from_static( - "gts.cf.core.rg.group_membership.v1~", + GROUP_MEMBERSHIP_RESOURCE_TYPE, &[pep_properties::OWNER_TENANT_ID], ); diff --git a/gears/system/resource-group/resource-group/src/domain/validation.rs b/gears/system/resource-group/resource-group/src/domain/validation.rs index 19434a9bf..f2554faf2 100644 --- a/gears/system/resource-group/resource-group/src/domain/validation.rs +++ b/gears/system/resource-group/resource-group/src/domain/validation.rs @@ -3,9 +3,10 @@ //! Shared domain validation utilities. use crate::domain::error::DomainError; +use toolkit_gts::gts_id; /// GTS type path prefix required for resource group types. -pub const RG_TYPE_PREFIX: &str = "gts.cf.core.rg.type.v1~"; +pub const RG_TYPE_PREFIX: &str = gts_id!("cf.core.rg.type.v1~"); /// Validate a GTS type code: non-empty, correct prefix, length limit. /// @@ -52,7 +53,7 @@ pub fn validate_type_code(code: &str) -> Result<(), DomainError> { /// types (e.g. `gts.cf.core.idp.user.v1~`, `gts.cf.vendor.lms.course.v1~`) /// and need not live in the RG type-registry namespace. /// -/// Format validation is delegated to [`gts::GtsID::new`], the canonical +/// Format validation is delegated to [`gts::GtsId::try_new`], the canonical /// GTS parser. Only **exact** GTS IDs (`gts.cf.core.idp.user.v1~`) are /// accepted; trailing-wildcard patterns (`gts.cf.core.am.*`) are /// rejected. `allowed_memberships` entries must resolve to a registered @@ -64,14 +65,14 @@ pub fn validate_type_code(code: &str) -> Result<(), DomainError> { /// Returns [`DomainError::validation`] if the code is not a valid GTS /// ID, or if it is a wildcard pattern. pub fn validate_membership_type_code(code: &str) -> Result<(), DomainError> { - let parsed = gts::GtsID::new(code).map_err(|e| { - DomainError::validation(format!("Invalid membership type code '{code}': {e}")) - })?; - if parsed.gts_id_segments.iter().any(|seg| seg.is_wildcard) { + if code.contains('*') { return Err(DomainError::validation(format!( "Membership type code '{code}' must be a concrete GTS type, not a wildcard pattern" ))); } + gts::GtsId::try_new(code).map_err(|e| { + DomainError::validation(format!("Invalid membership type code '{code}': {e}")) + })?; Ok(()) } diff --git a/gears/system/resource-group/resource-group/src/infra/storage/group_repo.rs b/gears/system/resource-group/resource-group/src/infra/storage/group_repo.rs index ef5c6dd14..0cfd0132e 100644 --- a/gears/system/resource-group/resource-group/src/infra/storage/group_repo.rs +++ b/gears/system/resource-group/resource-group/src/infra/storage/group_repo.rs @@ -204,7 +204,7 @@ impl GroupRepository { /// Resolve `type` string values to SMALLINT IDs in a validated `FilterNode`. /// /// Called AFTER `convert_expr_to_filter_node` validates the filter (String kind - /// for `type` field). Walks the tree and replaces `Value::String("gts...")` + /// for `type` field). Walks the tree and replaces GTS string values /// with `Value::Number(id)` for `GroupFilterField::Type` fields. The resolved /// numeric value is then handled by `filter_node_to_condition` which converts /// it to `sea_orm::Value::BigInt` — `PostgreSQL` implicitly casts to SMALLINT. diff --git a/gears/system/resource-group/resource-group/tests/api_rest_test.rs b/gears/system/resource-group/resource-group/tests/api_rest_test.rs index 40ec54d66..93a41ca80 100644 --- a/gears/system/resource-group/resource-group/tests/api_rest_test.rs +++ b/gears/system/resource-group/resource-group/tests/api_rest_test.rs @@ -9,6 +9,7 @@ mod common; use std::sync::Arc; +use toolkit_gts::gts_id; use async_trait::async_trait; use axum::Router; @@ -39,6 +40,12 @@ use cf_gears_resource_group::infra::storage::membership_repo::MembershipReposito use cf_gears_resource_group::infra::storage::migrations::Migrator; use cf_gears_resource_group::infra::storage::type_repo::TypeRepository; +macro_rules! rg_type_id { + ($($arg:tt)*) => { + format!("{}{}", gts_id!("cf.core.rg.type.v1~"), format_args!($($arg)*)) + }; +} + // ── Noop OpenAPI Registry for tests ───────────────────────────────────── struct NoopOpenApiRegistry; @@ -194,10 +201,7 @@ async fn response_body(resp: axum::http::Response) -> serde_json::Value { async fn create_type_returns_201() { let (router, _) = build_test_router().await; let tenant_id = Uuid::now_v7(); - let code = format!( - "gts.cf.core.rg.type.v1~test.api.{}.v1~", - Uuid::now_v7().as_simple() - ); + let code = rg_type_id!("test.api.{}.v1~", Uuid::now_v7().as_simple()); let req = json_request( "POST", @@ -223,10 +227,7 @@ async fn create_type_returns_201() { async fn create_type_duplicate_returns_409() { let (router, type_svc) = build_test_router().await; let tenant_id = Uuid::now_v7(); - let code = format!( - "gts.cf.core.rg.type.v1~test.dup.{}.v1~", - Uuid::now_v7().as_simple() - ); + let code = rg_type_id!("test.dup.{}.v1~", Uuid::now_v7().as_simple()); // Pre-create via service type_svc @@ -281,10 +282,7 @@ async fn create_type_invalid_code_returns_400() { async fn list_types_returns_200_with_page() { let (router, type_svc) = build_test_router().await; let tenant_id = Uuid::now_v7(); - let code = format!( - "gts.cf.core.rg.type.v1~test.list.{}.v1~", - Uuid::now_v7().as_simple() - ); + let code = rg_type_id!("test.list.{}.v1~", Uuid::now_v7().as_simple()); type_svc .create_type(resource_group_sdk::CreateTypeRequest { @@ -311,10 +309,7 @@ async fn list_types_returns_200_with_page() { async fn get_type_returns_200() { let (router, type_svc) = build_test_router().await; let tenant_id = Uuid::now_v7(); - let code = format!( - "gts.cf.core.rg.type.v1~test.get.{}.v1~", - Uuid::now_v7().as_simple() - ); + let code = rg_type_id!("test.get.{}.v1~", Uuid::now_v7().as_simple()); type_svc .create_type(resource_group_sdk::CreateTypeRequest { @@ -345,7 +340,7 @@ async fn get_type_returns_200() { async fn get_type_not_found_returns_404() { let (router, _) = build_test_router().await; let tenant_id = Uuid::now_v7(); - let code = "gts.cf.core.rg.type.v1~nonexistent.v1~"; + let code = gts_id!("cf.core.rg.type.v1~test.rg.nonexistent.type.v1~"); let encoded = code.replace('~', "%7E"); let req = json_request( @@ -362,10 +357,7 @@ async fn get_type_not_found_returns_404() { async fn delete_type_returns_204() { let (router, type_svc) = build_test_router().await; let tenant_id = Uuid::now_v7(); - let code = format!( - "gts.cf.core.rg.type.v1~test.del.{}.v1~", - Uuid::now_v7().as_simple() - ); + let code = rg_type_id!("test.del.{}.v1~", Uuid::now_v7().as_simple()); type_svc .create_type(resource_group_sdk::CreateTypeRequest { @@ -395,10 +387,7 @@ async fn delete_type_returns_204() { async fn create_group_returns_201() { let (router, type_svc) = build_test_router().await; let tenant_id = Uuid::now_v7(); - let type_code = format!( - "gts.cf.core.rg.type.v1~test.grp.{}.v1~", - Uuid::now_v7().as_simple() - ); + let type_code = rg_type_id!("test.grp.{}.v1~", Uuid::now_v7().as_simple()); type_svc .create_type(resource_group_sdk::CreateTypeRequest { @@ -527,10 +516,7 @@ fn assert_no_surrogate_ids(json: &serde_json::Value) { async fn rest_put_type_returns_200() { let (router, type_svc) = build_test_router().await; let tenant_id = Uuid::now_v7(); - let code = format!( - "gts.cf.core.rg.type.v1~test.put.{}.v1~", - Uuid::now_v7().as_simple() - ); + let code = rg_type_id!("test.put.{}.v1~", Uuid::now_v7().as_simple()); type_svc .create_type(resource_group_sdk::CreateTypeRequest { @@ -569,7 +555,7 @@ async fn rest_put_type_returns_200() { async fn rest_put_type_not_found_returns_404() { let (router, _) = build_test_router().await; let tenant_id = Uuid::now_v7(); - let code = "gts.cf.core.rg.type.v1~nonexistent.put.v1~"; + let code = gts_id!("cf.core.rg.type.v1~test.rg.nonexistent.put.v1~"); let encoded = code.replace('~', "%7E"); let req = json_request( @@ -594,10 +580,7 @@ async fn rest_post_membership_returns_201() { let tenant_id = Uuid::now_v7(); let ctx = make_ctx(tenant_id); - let mt_code = format!( - "gts.cf.core.rg.type.v1~test.mt2._.i{}.v1~", - Uuid::now_v7().as_simple() - ); + let mt_code = rg_type_id!("test.mt2._.i{}.v1~", Uuid::now_v7().as_simple()); type_svc .create_type(resource_group_sdk::CreateTypeRequest { code: mt_code.clone(), @@ -609,10 +592,7 @@ async fn rest_post_membership_returns_201() { .await .unwrap(); - let gt_code = format!( - "gts.cf.core.rg.type.v1~test.gt2.{}.v1~", - Uuid::now_v7().as_simple() - ); + let gt_code = rg_type_id!("test.gt2.{}.v1~", Uuid::now_v7().as_simple()); type_svc .create_type(resource_group_sdk::CreateTypeRequest { code: gt_code.clone(), @@ -662,11 +642,7 @@ async fn rest_post_membership_returns_201() { /// Helper: create a self-referencing root type (create, then update to allow self as parent). async fn create_self_ref_type(type_svc: &TypeService, suffix: &str) -> String { - let code = format!( - "gts.cf.core.rg.type.v1~test.{}.{}.v1~", - suffix, - Uuid::now_v7().as_simple() - ); + let code = rg_type_id!("test.{}.{}.v1~", suffix, Uuid::now_v7().as_simple()); type_svc .create_type(resource_group_sdk::CreateTypeRequest { code: code.clone(), @@ -734,10 +710,7 @@ async fn rest_delete_membership_returns_204() { let tenant_id = Uuid::now_v7(); let ctx = make_ctx(tenant_id); - let mt = format!( - "gts.cf.core.rg.type.v1~test.mtr._.i{}.v1~", - Uuid::now_v7().as_simple() - ); + let mt = rg_type_id!("test.mtr._.i{}.v1~", Uuid::now_v7().as_simple()); type_svc .create_type(resource_group_sdk::CreateTypeRequest { code: mt.clone(), @@ -749,10 +722,7 @@ async fn rest_delete_membership_returns_204() { .await .unwrap(); - let gt = format!( - "gts.cf.core.rg.type.v1~test.gtr.{}.v1~", - Uuid::now_v7().as_simple() - ); + let gt = rg_type_id!("test.gtr.{}.v1~", Uuid::now_v7().as_simple()); type_svc .create_type(resource_group_sdk::CreateTypeRequest { code: gt.clone(), @@ -819,10 +789,7 @@ async fn rest_post_group_with_parent_returns_201() { let tenant_id = Uuid::now_v7(); let ctx = make_ctx(tenant_id); - let root_type = format!( - "gts.cf.core.rg.type.v1~test.rtp.{}.v1~", - Uuid::now_v7().as_simple() - ); + let root_type = rg_type_id!("test.rtp.{}.v1~", Uuid::now_v7().as_simple()); // Create type first without self-reference, then update to allow self as parent type_svc .create_type(resource_group_sdk::CreateTypeRequest { @@ -1005,10 +972,7 @@ async fn rest_get_group_hierarchy_returns_200() { async fn rest_create_type_with_metadata_schema() { let (router, _) = build_test_router().await; let tenant_id = Uuid::now_v7(); - let code = format!( - "gts.cf.core.rg.type.v1~test.ms.{}.v1~", - Uuid::now_v7().as_simple() - ); + let code = rg_type_id!("test.ms.{}.v1~", Uuid::now_v7().as_simple()); let req = json_request( "POST", @@ -1045,10 +1009,7 @@ async fn rest_create_group_with_metadata() { let (router, type_svc, _, _) = build_shared_router().await; let tenant_id = Uuid::now_v7(); - let code = format!( - "gts.cf.core.rg.type.v1~test.gm.{}.v1~", - Uuid::now_v7().as_simple() - ); + let code = rg_type_id!("test.gm.{}.v1~", Uuid::now_v7().as_simple()); type_svc .create_type(resource_group_sdk::CreateTypeRequest { code: code.clone(), @@ -1084,10 +1045,7 @@ async fn rest_group_response_omits_null_metadata() { let (router, type_svc, _, _) = build_shared_router().await; let tenant_id = Uuid::now_v7(); - let code = format!( - "gts.cf.core.rg.type.v1~test.nm.{}.v1~", - Uuid::now_v7().as_simple() - ); + let code = rg_type_id!("test.nm.{}.v1~", Uuid::now_v7().as_simple()); type_svc .create_type(resource_group_sdk::CreateTypeRequest { code: code.clone(), @@ -1123,10 +1081,7 @@ async fn rest_group_response_omits_null_metadata() { async fn rest_type_response_omits_null_metadata_schema() { let (router, _) = build_test_router().await; let tenant_id = Uuid::now_v7(); - let code = format!( - "gts.cf.core.rg.type.v1~test.nms.{}.v1~", - Uuid::now_v7().as_simple() - ); + let code = rg_type_id!("test.nms.{}.v1~", Uuid::now_v7().as_simple()); let req = json_request( "POST", @@ -1163,7 +1118,7 @@ async fn input_create_type_non_rg_prefix_returns_400() { "POST", "/types-registry/v1/types", Some(serde_json::json!({ - "code": "gts.cf.other.prefix.v1~test.v1~", + "code": gts_id!("cf.other.prefix.type.v1~test.rg.other.type.v1~"), "can_be_root": true, "allowed_parent_types": [], "allowed_membership_types": [] @@ -1226,7 +1181,7 @@ async fn input_create_group_non_rg_type_returns_400() { "POST", "/resource-group/v1/groups", Some(serde_json::json!({ - "type": "gts.cf.other.prefix.v1~test.v1~", + "type": gts_id!("cf.other.prefix.type.v1~test.rg.other.type.v1~"), "name": "BadGroup" })), tenant_id, @@ -1270,10 +1225,7 @@ async fn input_membership_non_gts_resource_type() { let tenant_id = Uuid::now_v7(); let ctx = make_ctx(tenant_id); - let rt = format!( - "gts.cf.core.rg.type.v1~test.ngts.{}.v1~", - Uuid::now_v7().as_simple() - ); + let rt = rg_type_id!("test.ngts.{}.v1~", Uuid::now_v7().as_simple()); type_svc .create_type(resource_group_sdk::CreateTypeRequest { code: rt.clone(), @@ -1377,7 +1329,7 @@ async fn input_deser_type_can_be_root_string_returns_error() { "POST", "/types-registry/v1/types", Some(serde_json::json!({ - "code": "gts.cf.core.rg.type.v1~test.deser.v1~", + "code": gts_id!("cf.core.rg.type.v1~test.rg.deser.type.v1~"), "can_be_root": "yes", "allowed_parent_types": [], "allowed_membership_types": [] @@ -1402,7 +1354,7 @@ async fn input_deser_type_missing_can_be_root_returns_error() { "POST", "/types-registry/v1/types", Some(serde_json::json!({ - "code": "gts.cf.core.rg.type.v1~test.missing.v1~" + "code": gts_id!("cf.core.rg.type.v1~test.rg.missing.type.v1~") })), tenant_id, ); @@ -1446,7 +1398,7 @@ async fn input_deser_group_invalid_parent_uuid_returns_error() { "POST", "/resource-group/v1/groups", Some(serde_json::json!({ - "type": "gts.cf.core.rg.type.v1~test.v1~", + "type": gts_id!("cf.core.rg.type.v1~test.rg.simple.type.v1~"), "name": "BadParent", "parent_id": "not-a-uuid" })), @@ -1508,10 +1460,7 @@ async fn input_deser_group_empty_name_returns_400() { let (router, type_svc, _, _) = build_shared_router().await; let tenant_id = Uuid::now_v7(); - let code = format!( - "gts.cf.core.rg.type.v1~test.en.{}.v1~", - Uuid::now_v7().as_simple() - ); + let code = rg_type_id!("test.en.{}.v1~", Uuid::now_v7().as_simple()); type_svc .create_type(resource_group_sdk::CreateTypeRequest { code: code.clone(), @@ -1560,7 +1509,10 @@ async fn input_deser_membership_path_non_uuid_returns_400() { let req = json_request( "POST", - "/resource-group/v1/memberships/not-a-uuid/gts.cf.core.rg.type.v1~test.v1~/res-001", + &format!( + "/resource-group/v1/memberships/not-a-uuid/{}/res-001", + gts_id!("cf.core.rg.type.v1~test.rg.simple.type.v1~") + ), None, tenant_id, ); @@ -1573,10 +1525,7 @@ async fn input_deser_membership_path_non_uuid_returns_400() { async fn input_deser_extra_fields_behavior() { let (router, _) = build_test_router().await; let tenant_id = Uuid::now_v7(); - let code = format!( - "gts.cf.core.rg.type.v1~test.extra.{}.v1~", - Uuid::now_v7().as_simple() - ); + let code = rg_type_id!("test.extra.{}.v1~", Uuid::now_v7().as_simple()); let req = json_request( "POST", @@ -1613,10 +1562,7 @@ async fn gts_membership_post_tilde_encoded() { let tenant_id = Uuid::now_v7(); let ctx = make_ctx(tenant_id); - let mt = format!( - "gts.cf.core.rg.type.v1~test.tmt._.i{}.v1~", - Uuid::now_v7().as_simple() - ); + let mt = rg_type_id!("test.tmt._.i{}.v1~", Uuid::now_v7().as_simple()); type_svc .create_type(resource_group_sdk::CreateTypeRequest { code: mt.clone(), @@ -1628,10 +1574,7 @@ async fn gts_membership_post_tilde_encoded() { .await .unwrap(); - let gt = format!( - "gts.cf.core.rg.type.v1~test.tgt.{}.v1~", - Uuid::now_v7().as_simple() - ); + let gt = rg_type_id!("test.tgt.{}.v1~", Uuid::now_v7().as_simple()); type_svc .create_type(resource_group_sdk::CreateTypeRequest { code: gt.clone(), @@ -1679,10 +1622,7 @@ async fn gts_membership_delete_tilde_encoded() { let tenant_id = Uuid::now_v7(); let ctx = make_ctx(tenant_id); - let mt = format!( - "gts.cf.core.rg.type.v1~test.tmd._.i{}.v1~", - Uuid::now_v7().as_simple() - ); + let mt = rg_type_id!("test.tmd._.i{}.v1~", Uuid::now_v7().as_simple()); type_svc .create_type(resource_group_sdk::CreateTypeRequest { code: mt.clone(), @@ -1694,10 +1634,7 @@ async fn gts_membership_delete_tilde_encoded() { .await .unwrap(); - let gt = format!( - "gts.cf.core.rg.type.v1~test.tgd.{}.v1~", - Uuid::now_v7().as_simple() - ); + let gt = rg_type_id!("test.tgd.{}.v1~", Uuid::now_v7().as_simple()); type_svc .create_type(resource_group_sdk::CreateTypeRequest { code: gt.clone(), @@ -1749,10 +1686,7 @@ async fn gts_put_type_tilde_encoded() { let (router, type_svc, _, _) = build_shared_router().await; let tenant_id = Uuid::now_v7(); - let code = format!( - "gts.cf.core.rg.type.v1~test.tput.{}.v1~", - Uuid::now_v7().as_simple() - ); + let code = rg_type_id!("test.tput.{}.v1~", Uuid::now_v7().as_simple()); type_svc .create_type(resource_group_sdk::CreateTypeRequest { code: code.clone(), @@ -1785,10 +1719,7 @@ async fn smallint_type_response_has_no_surrogate_ids() { let (router, type_svc, _, _) = build_shared_router().await; let tenant_id = Uuid::now_v7(); - let code = format!( - "gts.cf.core.rg.type.v1~test.sid.{}.v1~", - Uuid::now_v7().as_simple() - ); + let code = rg_type_id!("test.sid.{}.v1~", Uuid::now_v7().as_simple()); type_svc .create_type(resource_group_sdk::CreateTypeRequest { code: code.clone(), @@ -1822,10 +1753,7 @@ async fn smallint_group_response_has_no_surrogate_ids() { let (router, type_svc, _, _) = build_shared_router().await; let tenant_id = Uuid::now_v7(); - let code = format!( - "gts.cf.core.rg.type.v1~test.gsid.{}.v1~", - Uuid::now_v7().as_simple() - ); + let code = rg_type_id!("test.gsid.{}.v1~", Uuid::now_v7().as_simple()); type_svc .create_type(resource_group_sdk::CreateTypeRequest { code: code.clone(), @@ -1862,10 +1790,7 @@ async fn smallint_membership_response_has_no_surrogate_ids() { let tenant_id = Uuid::now_v7(); let ctx = make_ctx(tenant_id); - let mt = format!( - "gts.cf.core.rg.type.v1~test.msid._.i{}.v1~", - Uuid::now_v7().as_simple() - ); + let mt = rg_type_id!("test.msid._.i{}.v1~", Uuid::now_v7().as_simple()); type_svc .create_type(resource_group_sdk::CreateTypeRequest { code: mt.clone(), @@ -1877,10 +1802,7 @@ async fn smallint_membership_response_has_no_surrogate_ids() { .await .unwrap(); - let gt = format!( - "gts.cf.core.rg.type.v1~test.gsidm.{}.v1~", - Uuid::now_v7().as_simple() - ); + let gt = rg_type_id!("test.gsidm.{}.v1~", Uuid::now_v7().as_simple()); type_svc .create_type(resource_group_sdk::CreateTypeRequest { code: gt.clone(), @@ -2018,10 +1940,7 @@ async fn rest_error_responses_have_problem_content_type_and_status() { assert!(body.get("backtrace").is_none(), "no backtrace leaked"); // --- 409 Conflict: duplicate type --- - let code = format!( - "gts.cf.core.rg.type.v1~test.errdup.{}.v1~", - Uuid::now_v7().as_simple() - ); + let code = rg_type_id!("test.errdup.{}.v1~", Uuid::now_v7().as_simple()); type_svc .create_type(resource_group_sdk::CreateTypeRequest { code: code.clone(), @@ -2083,7 +2002,7 @@ async fn rest_error_responses_have_problem_content_type_and_status() { "POST", "/resource-group/v1/groups", Some(serde_json::json!({ - "type": "gts.cf.core.rg.type.v1~nonexistent.v1~", + "type": gts_id!("cf.core.rg.type.v1~test.rg.nonexistent.type.v1~"), "name": "Ghost" })), tenant_id, @@ -2118,7 +2037,7 @@ async fn rest_route_smoke_all_endpoints_registered() { let (router, _, _, _) = build_shared_router().await; let tenant_id = Uuid::now_v7(); let fake_id = Uuid::now_v7(); - let fake_code = "gts.cf.core.rg.type.v1~smoke.v1%7E"; + let fake_code = rg_type_id!("smoke.v1%7E"); // (method, path, has_body?, description) let endpoints: Vec<(&str, String, bool, &str)> = vec![ diff --git a/gears/system/resource-group/resource-group/tests/authz_integration_test.rs b/gears/system/resource-group/resource-group/tests/authz_integration_test.rs index d8bfec591..a64fc826d 100644 --- a/gears/system/resource-group/resource-group/tests/authz_integration_test.rs +++ b/gears/system/resource-group/resource-group/tests/authz_integration_test.rs @@ -10,6 +10,7 @@ //! (`GroupService.list_groups` / `get_group` call enforcer internally) use std::sync::Arc; +use toolkit_gts::gts_id; use async_trait::async_trait; use uuid::Uuid; @@ -25,7 +26,7 @@ use toolkit_security::{SecurityContext, pep_properties}; // ── Resource type descriptor (mirrors what RG handlers will declare) ──── const RG_GROUP: ResourceType = ResourceType::from_static( - "gts.cf.core.rg.group.v1~", + gts_id!("cf.core.rg.group.v1~"), &[pep_properties::OWNER_TENANT_ID], ); diff --git a/gears/system/resource-group/resource-group/tests/common/mod.rs b/gears/system/resource-group/resource-group/tests/common/mod.rs index bec7d12d4..f53608487 100644 --- a/gears/system/resource-group/resource-group/tests/common/mod.rs +++ b/gears/system/resource-group/resource-group/tests/common/mod.rs @@ -7,6 +7,7 @@ //! and assertion utilities. Used by Phases 2-5 of the unit test plan. use std::sync::Arc; +use toolkit_gts::gts_id; use async_trait::async_trait; use uuid::Uuid; @@ -293,7 +294,8 @@ pub async fn create_root_type( // CamelCase without breaking the GTS regex); UUID-hex (prefixed with // `i` so it starts with a letter) goes in type. let code = format!( - "gts.cf.core.rg.type.v1~x.test.{}.i{}.v1~", + "{}x.test.{}.i{}.v1~", + gts_id!("cf.core.rg.type.v1~"), suffix.to_ascii_lowercase(), Uuid::now_v7().as_simple() ); @@ -320,7 +322,8 @@ pub async fn create_child_type( // CamelCase without breaking the GTS regex); UUID-hex (prefixed with // `i` so it starts with a letter) goes in type. let code = format!( - "gts.cf.core.rg.type.v1~x.test.{}.i{}.v1~", + "{}x.test.{}.i{}.v1~", + gts_id!("cf.core.rg.type.v1~"), suffix.to_ascii_lowercase(), Uuid::now_v7().as_simple() ); diff --git a/gears/system/resource-group/resource-group/tests/domain_unit_test.rs b/gears/system/resource-group/resource-group/tests/domain_unit_test.rs index 0667dbaca..80299a576 100644 --- a/gears/system/resource-group/resource-group/tests/domain_unit_test.rs +++ b/gears/system/resource-group/resource-group/tests/domain_unit_test.rs @@ -10,6 +10,7 @@ use cf_gears_resource_group::domain::error::DomainError; use cf_gears_resource_group::domain::validation::{self, RG_TYPE_PREFIX}; use toolkit_canonical_errors::{CanonicalError, Problem}; +use toolkit_gts::{GTS_ID_PREFIX, gts_id, gts_uri}; /// Build the wire `Problem` the canonical error middleware would emit /// for a given `DomainError`. The integration tests run without the @@ -22,18 +23,18 @@ fn wire(err: DomainError) -> Problem { // Canonical category problem-type URIs. See // `docs/arch/errors/categories/*.md`. -const NOT_FOUND_TYPE: &str = "gts://gts.cf.core.errors.err.v1~cf.core.err.not_found.v1~"; -const ALREADY_EXISTS_TYPE: &str = "gts://gts.cf.core.errors.err.v1~cf.core.err.already_exists.v1~"; +const NOT_FOUND_TYPE: &str = gts_uri!("cf.core.errors.err.v1~cf.core.err.not_found.v1~"); +const ALREADY_EXISTS_TYPE: &str = gts_uri!("cf.core.errors.err.v1~cf.core.err.already_exists.v1~"); const INVALID_ARGUMENT_TYPE: &str = - "gts://gts.cf.core.errors.err.v1~cf.core.err.invalid_argument.v1~"; + gts_uri!("cf.core.errors.err.v1~cf.core.err.invalid_argument.v1~"); const FAILED_PRECONDITION_TYPE: &str = - "gts://gts.cf.core.errors.err.v1~cf.core.err.failed_precondition.v1~"; + gts_uri!("cf.core.errors.err.v1~cf.core.err.failed_precondition.v1~"); const PERMISSION_DENIED_TYPE: &str = - "gts://gts.cf.core.errors.err.v1~cf.core.err.permission_denied.v1~"; -const INTERNAL_TYPE: &str = "gts://gts.cf.core.errors.err.v1~cf.core.err.internal.v1~"; + gts_uri!("cf.core.errors.err.v1~cf.core.err.permission_denied.v1~"); +const INTERNAL_TYPE: &str = gts_uri!("cf.core.errors.err.v1~cf.core.err.internal.v1~"); /// Resource-group GTS prefix (matches `RgError`'s `#[resource_error(...)]`). -const RG_GTS: &str = "gts.cf.core.resource_group.group.v1~"; +const RG_GTS: &str = gts_id!("cf.core.resource_group.group.v1~"); // ── validate_type_code ────────────────────────────────────────────────── @@ -102,7 +103,7 @@ fn validate_type_code_rejects_prefix_only() { fn validate_membership_type_code_accepts_non_rg_prefix() { // Per DESIGN.md, membership resource types are external domain types // and do NOT require the `gts.cf.core.rg.type.v1~` prefix. - let result = validation::validate_membership_type_code("gts.cf.core.idp.user.v1~"); + let result = validation::validate_membership_type_code(gts_id!("cf.core.idp.user.v1~")); assert!(result.is_ok(), "Expected ok, got {result:?}"); } @@ -128,7 +129,8 @@ fn validate_membership_type_code_rejects_empty() { fn validate_membership_type_code_rejects_trailing_wildcard_after_tilde() { // Wildcard patterns are not accepted: `gts_type_allowed_membership` // stores a SMALLINT FK to a concrete registered type, not a pattern. - let result = validation::validate_membership_type_code("gts.cf.core.rg.type.v1~*"); + let pattern = format!("{GTS_ID_PREFIX}cf.core.rg.type.v1~*"); + let result = validation::validate_membership_type_code(pattern.as_str()); assert!(result.is_err(), "Expected err, got {result:?}"); assert!(matches!( result.unwrap_err(), @@ -138,8 +140,8 @@ fn validate_membership_type_code_rejects_trailing_wildcard_after_tilde() { #[test] fn validate_membership_type_code_rejects_trailing_wildcard_after_dot() { - // `gts.cf.*` -- a wildcard pattern, rejected like any other wildcard. - let result = validation::validate_membership_type_code("gts.cf.*"); + let invalid_pattern = format!("{GTS_ID_PREFIX}cf.*"); + let result = validation::validate_membership_type_code(&invalid_pattern); assert!(result.is_err(), "Expected err, got {result:?}"); assert!(matches!( result.unwrap_err(), @@ -159,7 +161,8 @@ fn validate_membership_type_code_rejects_malformed_gts_path() { #[test] fn validate_membership_type_code_rejects_mid_string_wildcard() { - let result = validation::validate_membership_type_code("gts.cf.*.user.v1~"); + let invalid_pattern = format!("{GTS_ID_PREFIX}cf.*.user.v1~"); + let result = validation::validate_membership_type_code(&invalid_pattern); assert!(result.is_err()); assert!(matches!( result.unwrap_err(), @@ -945,7 +948,7 @@ fn domain_to_problem_conflict_is_409() { assert_eq!(problem.status, 409); assert_eq!( problem.problem_type, - "gts://gts.cf.core.errors.err.v1~cf.core.err.aborted.v1~" + gts_uri!("cf.core.errors.err.v1~cf.core.err.aborted.v1~") ); assert_eq!(problem.context["resource_type"], RG_GTS); assert_eq!(problem.context["reason"], "CONFLICT"); diff --git a/gears/system/resource-group/resource-group/tests/group_service_test.rs b/gears/system/resource-group/resource-group/tests/group_service_test.rs index 6fae5f817..d9016166d 100644 --- a/gears/system/resource-group/resource-group/tests/group_service_test.rs +++ b/gears/system/resource-group/resource-group/tests/group_service_test.rs @@ -12,6 +12,8 @@ mod common; use std::sync::Arc; +use toolkit_gts::GTS_ID_PREFIX; +use toolkit_gts::gts_id; use serde_json::json; use uuid::Uuid; @@ -237,7 +239,7 @@ async fn group_create_nonexistent_type() { &ctx, CreateGroupRequest { id: None, - code: "gts.cf.core.rg.type.v1~x.test.nonexistent.v1~".to_owned(), + code: gts_id!("cf.core.rg.type.v1~x.test.nonexistent.type.v1~").to_owned(), name: "Ghost".to_owned(), parent_id: None, metadata: None, @@ -559,7 +561,7 @@ async fn group_move_child_to_root() { // Create a type that can be both root and child let root_type = common::create_root_type(&type_svc, "org").await; let child_code = format!( - "gts.cf.core.rg.type.v1~x.test.flexible{}.v1~", + "{GTS_ID_PREFIX}cf.core.rg.type.v1~x.test.flexible{}.v1~", Uuid::now_v7().as_simple() ); let _flexible_type = type_svc @@ -681,7 +683,7 @@ async fn group_move_max_width_exceeded() { let root_type = common::create_root_type(&type_svc, "org").await; let child_code = format!( - "gts.cf.core.rg.type.v1~x.test.flex{}.v1~", + "{GTS_ID_PREFIX}cf.core.rg.type.v1~x.test.flex{}.v1~", Uuid::now_v7().as_simple() ); type_svc @@ -1288,7 +1290,7 @@ async fn group_move_max_depth_exceeded() { let child_type = common::create_child_type(&type_svc, "dept", &[&root_type.code], &[]).await; // sub_type allows child_type as parent, can also be root let sub_code = format!( - "gts.cf.core.rg.type.v1~x.test.sub{}.v1~", + "{GTS_ID_PREFIX}cf.core.rg.type.v1~x.test.sub{}.v1~", Uuid::now_v7().as_simple() ); type_svc @@ -1320,7 +1322,7 @@ async fn group_move_max_depth_exceeded() { common::create_root_group(&group_svc, &ctx, &sub_code, "Standalone", tenant_id).await; // sub needs a type that allows sub_code as parent -- create another type for that let subsub_code = format!( - "gts.cf.core.rg.type.v1~x.test.subsub{}.v1~", + "{GTS_ID_PREFIX}cf.core.rg.type.v1~x.test.subsub{}.v1~", Uuid::now_v7().as_simple() ); type_svc @@ -1929,7 +1931,7 @@ async fn create_adr_types( let course_type = common::create_root_type(type_svc, "adrcourse").await; let suffix_t = format!("adrtenant{}", uuid::Uuid::now_v7().as_simple()); - let tenant_code = format!("gts.cf.core.rg.type.v1~x.test.{suffix_t}.v1~"); + let tenant_code = format!("{}x.test.{suffix_t}.v1~", gts_id!("cf.core.rg.type.v1~")); // Tenant type: create first without self-reference, then update type_svc diff --git a/gears/system/resource-group/resource-group/tests/membership_service_test.rs b/gears/system/resource-group/resource-group/tests/membership_service_test.rs index e06e0ccba..f56fc6798 100644 --- a/gears/system/resource-group/resource-group/tests/membership_service_test.rs +++ b/gears/system/resource-group/resource-group/tests/membership_service_test.rs @@ -23,6 +23,7 @@ use cf_gears_resource_group::infra::storage::type_repo::TypeRepository; use resource_group_sdk::CreateTypeRequest; use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; use toolkit_db::secure::SecureEntityExt; +use toolkit_gts::{GTS_ID_PREFIX, gts_id}; use toolkit_security::AccessScope; /// Helper: create a root type that allows the given membership type paths. @@ -32,7 +33,7 @@ async fn create_type_with_memberships( memberships: &[&str], ) -> resource_group_sdk::ResourceGroupType { let code = format!( - "gts.cf.core.rg.type.v1~x.test.{}{}.v1~", + "{GTS_ID_PREFIX}cf.core.rg.type.v1~x.test.{}{}.v1~", suffix, Uuid::now_v7().as_simple() ); @@ -168,7 +169,12 @@ async fn membership_add_unregistered_resource_type() { // Try to add membership with a type path that is NOT registered in gts_type table let err = mbr_svc - .add_membership(&ctx, group.id, "gts.cf.fake.nonexistent.v1~", "res-001") + .add_membership( + &ctx, + group.id, + gts_id!("cf.fake.nonexistent.type.v1~"), + "res-001", + ) .await .expect_err("unregistered type should fail"); @@ -423,7 +429,12 @@ async fn membership_remove_unregistered_resource_type() { let group = common::create_root_group(&group_svc, &ctx, &grp_type.code, "G1", tenant).await; let err = mbr_svc - .remove_membership(&ctx, group.id, "gts.cf.fake.unregistered.v1~", "res-001") + .remove_membership( + &ctx, + group.id, + gts_id!("cf.fake.unregistered.type.v1~"), + "res-001", + ) .await .expect_err("remove with unregistered type should fail"); diff --git a/gears/system/resource-group/resource-group/tests/read_service_test.rs b/gears/system/resource-group/resource-group/tests/read_service_test.rs index 5b3d44ae0..64e59815f 100644 --- a/gears/system/resource-group/resource-group/tests/read_service_test.rs +++ b/gears/system/resource-group/resource-group/tests/read_service_test.rs @@ -22,6 +22,7 @@ use cf_gears_resource_group::domain::read_service::RgReadService; use cf_gears_resource_group::domain::type_service::TypeService; use cf_gears_resource_group::infra::storage::type_repo::TypeRepository; use resource_group_sdk::{CreateTypeRequest, ResourceGroupReadHierarchy, ResourceGroupType}; +use toolkit_gts::GTS_ID_PREFIX; /// Root type (`can_be_root = true`) that accepts the given membership types. async fn root_type_with_memberships( @@ -30,7 +31,7 @@ async fn root_type_with_memberships( memberships: &[&str], ) -> ResourceGroupType { let code = format!( - "gts.cf.core.rg.type.v1~x.test.{}{}.v1~", + "{GTS_ID_PREFIX}cf.core.rg.type.v1~x.test.{}{}.v1~", suffix, Uuid::now_v7().as_simple() ); diff --git a/gears/system/resource-group/resource-group/tests/seeding_test.rs b/gears/system/resource-group/resource-group/tests/seeding_test.rs index 33ed35473..5b10e698d 100644 --- a/gears/system/resource-group/resource-group/tests/seeding_test.rs +++ b/gears/system/resource-group/resource-group/tests/seeding_test.rs @@ -12,6 +12,7 @@ mod common; use std::sync::Arc; +use toolkit_gts::GTS_ID_PREFIX; use common::{make_ctx, make_group_service, make_membership_service, test_db}; use uuid::Uuid; @@ -48,7 +49,7 @@ fn unique_type_code(suffix: &str) -> String { // (lowercased); UUID-hex (with `i` prefix to start with a letter) // goes in type. format!( - "gts.cf.core.rg.type.v1~x.test.{}.i{}.v1~", + "{GTS_ID_PREFIX}cf.core.rg.type.v1~x.test.{}.i{}.v1~", suffix.to_ascii_lowercase(), Uuid::now_v7().as_simple() ) diff --git a/gears/system/resource-group/resource-group/tests/tenant_filtering_db_test.rs b/gears/system/resource-group/resource-group/tests/tenant_filtering_db_test.rs index 2b1c5d063..8b95d689d 100644 --- a/gears/system/resource-group/resource-group/tests/tenant_filtering_db_test.rs +++ b/gears/system/resource-group/resource-group/tests/tenant_filtering_db_test.rs @@ -12,6 +12,7 @@ mod common; use std::sync::Arc; +use toolkit_gts::GTS_ID_PREFIX; use async_trait::async_trait; use uuid::Uuid; @@ -490,11 +491,11 @@ async fn group_based_membership_data_correctly_stored() { // Create types: project (root, allows "task" members) and task let project_type = format!( - "gts.cf.core.rg.type.v1~x.test.proj.i{}.v1~", + "{GTS_ID_PREFIX}cf.core.rg.type.v1~x.test.proj.i{}.v1~", Uuid::now_v7().as_simple() ); let task_type = format!( - "gts.cf.core.rg.type.v1~x.test.task.i{}.v1~", + "{GTS_ID_PREFIX}cf.core.rg.type.v1~x.test.task.i{}.v1~", Uuid::now_v7().as_simple() ); diff --git a/gears/system/resource-group/resource-group/tests/type_service_test.rs b/gears/system/resource-group/resource-group/tests/type_service_test.rs index 9e9b1b4f1..9a5c3e332 100644 --- a/gears/system/resource-group/resource-group/tests/type_service_test.rs +++ b/gears/system/resource-group/resource-group/tests/type_service_test.rs @@ -10,6 +10,7 @@ mod common; use std::sync::Arc; +use toolkit_gts::GTS_ID_PREFIX; use serde_json::json; use uuid::Uuid; @@ -31,7 +32,7 @@ use toolkit_security::AccessScope; /// Generate a unique GTS type code with the given suffix. fn type_code(suffix: &str) -> String { format!( - "gts.cf.core.rg.type.v1~x.test.{}{}.v1~", + "{GTS_ID_PREFIX}cf.core.rg.type.v1~x.test.{}{}.v1~", suffix, Uuid::now_v7().as_simple() ) diff --git a/gears/system/tenant-resolver/tenant-resolver-sdk/src/gts.rs b/gears/system/tenant-resolver/tenant-resolver-sdk/src/gts.rs index 444f65c9d..ead9a0d5d 100644 --- a/gears/system/tenant-resolver/tenant-resolver-sdk/src/gts.rs +++ b/gears/system/tenant-resolver/tenant-resolver-sdk/src/gts.rs @@ -41,7 +41,7 @@ use toolkit_gts::gts_type_schema; #[gts_type_schema( dir_path = "schemas", base = PluginV1, - type_id = "gts.cf.toolkit.plugins.plugin.v1~cf.core.tenant_resolver.plugin.v1~", + type_id = gts_id!("cf.toolkit.plugins.plugin.v1~cf.core.tenant_resolver.plugin.v1~"), description = "Tenant Resolver plugin specification", properties = "", )] diff --git a/gears/system/types-registry/types-registry-sdk/Cargo.toml b/gears/system/types-registry/types-registry-sdk/Cargo.toml index c72f482c1..ea52507b5 100644 --- a/gears/system/types-registry/types-registry-sdk/Cargo.toml +++ b/gears/system/types-registry/types-registry-sdk/Cargo.toml @@ -43,3 +43,4 @@ serde_json = { workspace = true } # GTS types (from git dependency) gts = { workspace = true } +toolkit-gts = { workspace = true } diff --git a/gears/system/types-registry/types-registry-sdk/src/api.rs b/gears/system/types-registry/types-registry-sdk/src/api.rs index 47e50a6bf..c4bdcbfe9 100644 --- a/gears/system/types-registry/types-registry-sdk/src/api.rs +++ b/gears/system/types-registry/types-registry-sdk/src/api.rs @@ -17,7 +17,7 @@ use crate::models::{GtsInstance, GtsTypeSchema, InstanceQuery, RegisterResult, T /// This trait can be consumed by other gears via `ClientHub`: /// ```ignore /// let client = hub.get::()?; -/// let schema = client.get_type_schema("gts.acme.core.events.user.v1~").await?; +/// let schema = client.get_type_schema(gts_id!("acme.core.events.user.v1~")).await?; /// ``` /// /// GTS type-schemas and instances are global resources (not tenant-scoped), diff --git a/gears/system/types-registry/types-registry-sdk/src/error_tests.rs b/gears/system/types-registry/types-registry-sdk/src/error_tests.rs index 71ace4152..7b2c61bcd 100644 --- a/gears/system/types-registry/types-registry-sdk/src/error_tests.rs +++ b/gears/system/types-registry/types-registry-sdk/src/error_tests.rs @@ -21,6 +21,7 @@ mod wire_vocabulary_round_trip { use crate::gts::{self, TypeResource}; use crate::{field, precondition}; use toolkit_canonical_errors::{CanonicalError, Problem}; + use toolkit_gts::gts_id; fn problem(err: CanonicalError) -> serde_json::Value { serde_json::to_value(Problem::from(err)).expect("Problem serializes") @@ -63,9 +64,11 @@ mod wire_vocabulary_round_trip { #[test] fn parent_not_registered_type_round_trips_to_violations() { let err = TypeResource::failed_precondition() - .with_resource("gts.acme.core.events.base.v1~acme.x.derived.v1.0~") + .with_resource(gts_id!( + "acme.core.events.base.v1~acme.x.events.derived.v2~" + )) .with_precondition_violation( - "gts.acme.core.events.base.v1~", + gts_id!("acme.core.events.base.v1~"), "required type-schema is not registered", precondition::PARENT_NOT_REGISTERED, ) @@ -77,7 +80,8 @@ mod wire_vocabulary_round_trip { "type must round-trip into violations[].type", ); assert_eq!( - json["context"]["violations"][0]["subject"], "gts.acme.core.events.base.v1~", + json["context"]["violations"][0]["subject"], + gts_id!("acme.core.events.base.v1~"), "parent id must round-trip into violations[].subject", ); } @@ -94,6 +98,7 @@ mod projection_tests { use crate::gts::{self, TypeResource}; use crate::precondition; use toolkit_canonical_errors::{CanonicalError, Problem}; + use toolkit_gts::gts_id; #[test] fn invalid_argument_projects_typed_validation_reason() { @@ -114,7 +119,7 @@ mod projection_tests { #[test] fn not_found_projects_resource_type_and_name() { let canonical = TypeResource::not_found("type schema not found") - .with_resource("gts.acme.core.events.test.v1~") + .with_resource(gts_id!("acme.core.events.test.v1~")) .create(); match TypesRegistryError::from(canonical) { TypesRegistryError::NotFound { @@ -123,7 +128,7 @@ mod projection_tests { .. } => { assert_eq!(resource_type, gts::TYPE_RESOURCE_TYPE); - assert_eq!(name, "gts.acme.core.events.test.v1~"); + assert_eq!(name, gts_id!("acme.core.events.test.v1~")); } other => panic!("expected NotFound, got {other:?}"), } @@ -132,7 +137,7 @@ mod projection_tests { #[test] fn already_exists_projects_resource_type_and_name() { let canonical = TypeResource::already_exists("entity exists") - .with_resource("gts.acme.core.events.test.v1~") + .with_resource(gts_id!("acme.core.events.test.v1~")) .create(); match TypesRegistryError::from(canonical) { TypesRegistryError::AlreadyExists { @@ -141,7 +146,7 @@ mod projection_tests { .. } => { assert_eq!(resource_type, gts::TYPE_RESOURCE_TYPE); - assert_eq!(name, "gts.acme.core.events.test.v1~"); + assert_eq!(name, gts_id!("acme.core.events.test.v1~")); } other => panic!("expected AlreadyExists, got {other:?}"), } @@ -156,7 +161,7 @@ mod projection_tests { fn not_found_without_resource_type_falls_through_to_other() { let canonical = malformed_without_resource_type( TypeResource::not_found("missing") - .with_resource("gts.acme.core.events.test.v1~") + .with_resource(gts_id!("acme.core.events.test.v1~")) .create(), ); assert!( @@ -181,7 +186,7 @@ mod projection_tests { fn already_exists_without_resource_type_falls_through_to_other() { let canonical = malformed_without_resource_type( TypeResource::already_exists("entity exists") - .with_resource("gts.acme.core.events.test.v1~") + .with_resource(gts_id!("acme.core.events.test.v1~")) .create(), ); assert!( @@ -216,9 +221,11 @@ mod projection_tests { #[test] fn failed_precondition_projects_parent_not_registered() { let canonical = TypeResource::failed_precondition() - .with_resource("gts.acme.core.events.base.v1~acme.x.derived.v1.0~") + .with_resource(gts_id!( + "acme.core.events.base.v1~acme.x.events.derived.v2~" + )) .with_precondition_violation( - "gts.acme.core.events.base.v1~", + gts_id!("acme.core.events.base.v1~"), "required type-schema is not registered", precondition::PARENT_NOT_REGISTERED, ) @@ -229,10 +236,10 @@ mod projection_tests { dependent_id, detail, } => { - assert_eq!(parent_type_id, "gts.acme.core.events.base.v1~"); + assert_eq!(parent_type_id, gts_id!("acme.core.events.base.v1~")); assert_eq!( dependent_id, - "gts.acme.core.events.base.v1~acme.x.derived.v1.0~" + gts_id!("acme.core.events.base.v1~acme.x.events.derived.v2~") ); assert_eq!(detail, "required type-schema is not registered"); } @@ -297,9 +304,11 @@ mod projection_tests { // in-process ClientHub caller — exercised on the lossless // parent-not-registered encoding. let canonical = TypeResource::failed_precondition() - .with_resource("gts.acme.core.events.base.v1~acme.x.derived.v1.0~") + .with_resource(gts_id!( + "acme.core.events.base.v1~acme.x.events.derived.v2~" + )) .with_precondition_violation( - "gts.acme.core.events.base.v1~", + gts_id!("acme.core.events.base.v1~"), "required type-schema is not registered", precondition::PARENT_NOT_REGISTERED, ) @@ -315,10 +324,10 @@ mod projection_tests { dependent_id, .. } => { - assert_eq!(parent_type_id, "gts.acme.core.events.base.v1~"); + assert_eq!(parent_type_id, gts_id!("acme.core.events.base.v1~")); assert_eq!( dependent_id, - "gts.acme.core.events.base.v1~acme.x.derived.v1.0~" + gts_id!("acme.core.events.base.v1~acme.x.events.derived.v2~") ); } other => panic!("expected ParentNotRegistered after round-trip, got {other:?}"), diff --git a/gears/system/types-registry/types-registry-sdk/src/gts.rs b/gears/system/types-registry/types-registry-sdk/src/gts.rs index c578d06c9..4040fd2cf 100644 --- a/gears/system/types-registry/types-registry-sdk/src/gts.rs +++ b/gears/system/types-registry/types-registry-sdk/src/gts.rs @@ -14,14 +14,15 @@ //! `CanonicalError` keeps the SDK on a single error type end-to-end). use toolkit_canonical_errors::resource_error; +use toolkit_gts::gts_id; /// The canonical GTS resource type for types-registry entities. Lands in /// `CanonicalError` `resource_type` / the wire `context.resource_type`. -pub const TYPE_RESOURCE_TYPE: &str = "gts.cf.types_registry.registry.type.v1~"; +pub const TYPE_RESOURCE_TYPE: &str = gts_id!("cf.types_registry.registry.type.v1~"); /// SDK-internal canonical-error scope. Mirrors the impl crate's /// `#[resource_error]` marker so the SDK's `try_new` constructors emit the same /// `resource_type` the REST ladder does. Its literal MUST equal /// [`TYPE_RESOURCE_TYPE`] — pinned by `gts_resource_type_round_trips`. -#[resource_error("gts.cf.types_registry.registry.type.v1~")] +#[resource_error(gts_id!("cf.types_registry.registry.type.v1~"))] pub(crate) struct TypeResource; diff --git a/gears/system/types-registry/types-registry-sdk/src/lib.rs b/gears/system/types-registry/types-registry-sdk/src/lib.rs index 8b6518db8..6a1f69923 100644 --- a/gears/system/types-registry/types-registry-sdk/src/lib.rs +++ b/gears/system/types-registry/types-registry-sdk/src/lib.rs @@ -19,9 +19,9 @@ //! //! let client = hub.get::()?; //! -//! let schema = client.get_type_schema("gts.acme.core.events.user.v1~").await?; +//! let schema = client.get_type_schema(gts_id!("acme.core.events.user.v1~")).await?; //! let schemas = client -//! .list_type_schemas(TypeSchemaQuery::default().with_pattern("gts.acme.*")) +//! .list_type_schemas(TypeSchemaQuery::default().with_pattern(format!("{GTS_ID_PREFIX}acme.*"))) //! .await?; //! ``` //! diff --git a/gears/system/types-registry/types-registry-sdk/src/models.rs b/gears/system/types-registry/types-registry-sdk/src/models.rs index eb842e14c..637b5f9d5 100644 --- a/gears/system/types-registry/types-registry-sdk/src/models.rs +++ b/gears/system/types-registry/types-registry-sdk/src/models.rs @@ -5,8 +5,9 @@ use std::collections::BTreeMap; use std::sync::Arc; +use toolkit_gts::GTS_ID_URI_PREFIX; -use gts::{GtsID, GtsIdSegment, GtsInstanceId}; +use gts::{GtsId, GtsIdSegment, GtsInstanceId}; use serde_json::{Map, Value}; use toolkit_canonical_errors::CanonicalError; @@ -164,9 +165,9 @@ impl GtsTypeSchema { _ => {} } let parsed = - GtsID::new(type_id.as_ref()).map_err(|e| invalid_gts_id_error(format!("{e}")))?; + GtsId::try_new(type_id.as_ref()).map_err(|e| invalid_gts_id_error(format!("{e}")))?; let type_uuid = parsed.to_uuid(); - let segments = parsed.gts_id_segments; + let segments = parsed.segments().to_vec(); let traits = Self::extract_traits(&raw_schema); let traits_schema = Self::extract_traits_schema(&raw_schema); let title = Self::extract_title(&raw_schema); @@ -183,7 +184,7 @@ impl GtsTypeSchema { }) } - /// Derives the GTS parent's `type_id` by stripping the last `~`-segment. + /// Derives the GTS parent's `type_id` from the parsed GTS chain. /// /// Mirrors gts-rust's chain semantics: for a chained type id like /// `gts.cf.core.events.type.v1~x.commerce.orders.order.v1.0~`, the parent @@ -191,9 +192,11 @@ impl GtsTypeSchema { /// type-schemas or for ids that don't end with `~`. #[must_use] pub fn derive_parent_type_id(type_id: &str) -> Option { - let trimmed = type_id.strip_suffix('~')?; - let last_tilde = trimmed.rfind('~')?; - Some(GtsTypeId::new(&type_id[..=last_tilde])) + let parsed = GtsId::try_new(type_id).ok()?; + if !parsed.is_type() { + return None; + } + parsed.get_type_id().map(|id| GtsTypeId::new(&id)) } /// Reads `x-gts-traits` from the top level of a schema value. @@ -216,7 +219,7 @@ impl GtsTypeSchema { }; arr.iter() .filter_map(|item| item.get("$ref").and_then(|r| r.as_str())) - .map(|r| r.strip_prefix("gts://").unwrap_or(r).to_owned()) + .map(|r| r.strip_prefix(GTS_ID_URI_PREFIX).unwrap_or(r).to_owned()) .collect() } @@ -238,7 +241,7 @@ impl GtsTypeSchema { /// Returns the vendor from the primary segment. #[must_use] pub fn vendor(&self) -> Option<&str> { - self.primary_segment().map(|s| s.vendor.as_str()) + self.primary_segment().map(GtsIdSegment::vendor) } /// Iteration over the inheritance chain (this schema first, then parent, @@ -454,7 +457,7 @@ fn merge_schema_with_parent(schema: &Value, parent: Option<&GtsTypeSchema>) -> V && obj.len() == 1 && let Some(ref_uri) = obj.get("$ref").and_then(|r| r.as_str()) && { - let target = ref_uri.strip_prefix("gts://").unwrap_or(ref_uri); + let target = ref_uri.strip_prefix(GTS_ID_URI_PREFIX).unwrap_or(ref_uri); parent.type_id == target } { let mut merged = parent.effective_schema(); @@ -542,9 +545,10 @@ impl GtsInstance { type_schema.type_id ))); } - let parsed = GtsID::new(id.as_ref()).map_err(|e| invalid_gts_id_error(format!("{e}")))?; + let parsed = + GtsId::try_new(id.as_ref()).map_err(|e| invalid_gts_id_error(format!("{e}")))?; let uuid = parsed.to_uuid(); - let segments = parsed.gts_id_segments; + let segments = parsed.segments().to_vec(); Ok(Self { uuid, id, @@ -563,11 +567,13 @@ impl GtsInstance { /// Derives the type-schema (parent type) GTS ID from an instance `id`. /// - /// Returns everything up to and including the last `~`. `None` when the - /// `id` contains no `~`. + /// Returns the parsed type-schema segment for the instance id. #[must_use] pub fn derive_type_id(id: &str) -> Option { - id.rfind('~').map(|i| GtsTypeId::new(&id[..=i])) + GtsId::try_new(id) + .ok() + .and_then(|parsed| parsed.get_type_id()) + .map(|id| GtsTypeId::new(&id)) } /// Returns the primary segment (first segment in the chain). @@ -579,7 +585,7 @@ impl GtsInstance { /// Returns the vendor from the primary segment. #[must_use] pub fn vendor(&self) -> Option<&str> { - self.primary_segment().map(|s| s.vendor.as_str()) + self.primary_segment().map(GtsIdSegment::vendor) } } diff --git a/gears/system/types-registry/types-registry-sdk/src/models_tests.rs b/gears/system/types-registry/types-registry-sdk/src/models_tests.rs index 209eb930f..dc70c3bab 100644 --- a/gears/system/types-registry/types-registry-sdk/src/models_tests.rs +++ b/gears/system/types-registry/types-registry-sdk/src/models_tests.rs @@ -12,6 +12,7 @@ use super::*; use serde_json::json; use toolkit_canonical_errors::{CanonicalError, InvalidArgument}; +use toolkit_gts::{GTS_ID_PREFIX, gts_id, gts_uri}; /// `try_new` now returns `CanonicalError`; the legacy `is_invalid_gts_*` /// predicates are gone. The kind distinction collapsed at the canonical @@ -38,10 +39,10 @@ fn make_type_schema( } // Reusable chain-form ids (string-shape valid for derive_parent_type_id). -const BASE_ID: &str = "gts.acme.core.events.base.v1~"; -const DERIVED_ID: &str = "gts.acme.core.events.base.v1~acme.core.events.derived.v1.0~"; +const BASE_ID: &str = gts_id!("acme.core.events.base.v1~"); +const DERIVED_ID: &str = gts_id!("acme.core.events.base.v1~acme.core.events.derived.v1.0~"); const LEAF_ID: &str = - "gts.acme.core.events.base.v1~acme.core.events.derived.v1.0~vendor.x.y.leaf.v1.0~"; + gts_id!("acme.core.events.base.v1~acme.core.events.derived.v1.0~vendor.x.y.leaf.v1.0~"); #[test] fn test_type_schema_try_new_extracts_traits() { @@ -75,13 +76,16 @@ fn test_derive_parent_type_id() { Some(DERIVED_ID.to_owned()) ); // Instance (no trailing `~`) → no parent (helper is type-schemas-only). - assert!(GtsTypeSchema::derive_parent_type_id("gts.foo.bar.baz.v1~inst").is_none()); + let invalid_instance = format!("{GTS_ID_PREFIX}foo.bar.baz.v1~inst"); + assert!(GtsTypeSchema::derive_parent_type_id(&invalid_instance).is_none()); } #[test] fn test_type_schema_rejects_instance_id() { let err = GtsTypeSchema::try_new( - GtsTypeId::new("gts.acme.core.events.user.v1~acme.core.instances.u1.v1"), + GtsTypeId::new(gts_id!( + "acme.core.events.user.v1~acme.core.instances.u1.v1" + )), json!({}), None, None, @@ -92,11 +96,11 @@ fn test_type_schema_rejects_instance_id() { #[test] fn test_type_schema_rejects_mismatched_parent() { - // BASE_ID and "gts.contoso.vendor.events.x.v1~" are unrelated; passing + // BASE_ID and the Contoso parent are unrelated; passing // the wrong one as parent of DERIVED_ID must error rather than // silently corrupt the chain. let wrong_parent = Arc::new(make_type_schema( - "gts.contoso.vendor.events.base.v1~", + gts_id!("contoso.vendor.events.base.v1~"), json!({}), None, )); @@ -140,7 +144,7 @@ fn test_effective_properties_deep_merge() { DERIVED_ID, json!({ "allOf": [ - { "$ref": format!("gts://{BASE_ID}") }, + { "$ref": gts_uri!(BASE_ID) }, { "properties": { "name": { "type": "string" } } } ] }), @@ -161,7 +165,7 @@ fn test_effective_properties_child_overrides_parent() { let child = make_type_schema( DERIVED_ID, json!({ - "allOf": [{ "$ref": format!("gts://{BASE_ID}") }], + "allOf": [{ "$ref": gts_uri!(BASE_ID) }], "properties": { "field": { "type": "string", "title": "from-child" } } }), Some(base), @@ -185,7 +189,7 @@ fn test_effective_required_dedup() { DERIVED_ID, json!({ "allOf": [ - { "$ref": format!("gts://{BASE_ID}") }, + { "$ref": gts_uri!(BASE_ID) }, { "required": ["name", "id"] } ] }), @@ -244,7 +248,7 @@ fn test_effective_schema_inlines_refs() { DERIVED_ID, json!({ "allOf": [ - { "$ref": format!("gts://{BASE_ID}") }, + { "$ref": gts_uri!(BASE_ID) }, { "properties": { "name": { "type": "string" } } } ] }), @@ -274,7 +278,7 @@ fn test_effective_properties_three_level_chain() { let parent = Arc::new(make_type_schema( DERIVED_ID, json!({ - "allOf": [{ "$ref": format!("gts://{BASE_ID}") }], + "allOf": [{ "$ref": gts_uri!(BASE_ID) }], "properties": { "b": { "type": "string", "title": "from-parent" }, "shared": { "type": "string", "title": "from-parent" } @@ -285,7 +289,7 @@ fn test_effective_properties_three_level_chain() { let child = make_type_schema( LEAF_ID, json!({ - "allOf": [{ "$ref": format!("gts://{DERIVED_ID}") }], + "allOf": [{ "$ref": gts_uri!(DERIVED_ID) }], "properties": { "c": { "type": "string", "title": "from-child" }, "shared": { "type": "string", "title": "from-child" } @@ -338,7 +342,7 @@ fn test_effective_properties_from_allof_inline_overlay() { DERIVED_ID, json!({ "allOf": [ - { "$ref": format!("gts://{BASE_ID}") }, + { "$ref": gts_uri!(BASE_ID) }, { "properties": { "from_overlay": { "type": "string" } } } ] }), @@ -361,7 +365,7 @@ fn test_effective_required_three_level_dedup_and_order() { let parent = Arc::new(make_type_schema( DERIVED_ID, json!({ - "allOf": [{ "$ref": format!("gts://{BASE_ID}") }], + "allOf": [{ "$ref": gts_uri!(BASE_ID) }], "required": ["name", "id"] }), Some(grand), @@ -369,7 +373,7 @@ fn test_effective_required_three_level_dedup_and_order() { let child = make_type_schema( LEAF_ID, json!({ - "allOf": [{ "$ref": format!("gts://{DERIVED_ID}") }], + "allOf": [{ "$ref": gts_uri!(DERIVED_ID) }], "required": ["age", "name"] }), Some(parent), @@ -648,7 +652,7 @@ fn test_effective_schema_strips_id_and_schema_from_inlined_parent() { let base = Arc::new(make_type_schema( BASE_ID, json!({ - "$id": format!("gts://{BASE_ID}"), + "$id": gts_uri!(BASE_ID), "$schema": "https://json-schema.org/draft-07/schema#", "type": "object", "properties": { "id": { "type": "string" } } @@ -658,7 +662,7 @@ fn test_effective_schema_strips_id_and_schema_from_inlined_parent() { let child = make_type_schema( DERIVED_ID, json!({ - "allOf": [{ "$ref": format!("gts://{BASE_ID}") }] + "allOf": [{ "$ref": gts_uri!(BASE_ID) }] }), Some(base), ); @@ -675,13 +679,13 @@ fn test_effective_schema_leaves_non_parent_refs_alone() { // A `$ref` in `allOf` that points to something OTHER than the GTS // parent (a "mixin") is left as-is — only the parent's body is inlined. let base = Arc::new(make_type_schema(BASE_ID, json!({"type": "object"}), None)); - let mixin_id = "gts.contoso.vendor.events.mixin.v1~"; + let mixin_id = gts_id!("contoso.vendor.events.mixin.v1~"); let child = make_type_schema( DERIVED_ID, json!({ "allOf": [ - { "$ref": format!("gts://{BASE_ID}") }, - { "$ref": format!("gts://{mixin_id}") } + { "$ref": gts_uri!(BASE_ID) }, + { "$ref": gts_uri!(mixin_id) } ] }), Some(base), @@ -693,7 +697,7 @@ fn test_effective_schema_leaves_non_parent_refs_alone() { // Mixin ref preserved. assert_eq!( all_of[1]["$ref"].as_str(), - Some(format!("gts://{mixin_id}").as_str()) + Some(gts_uri!(mixin_id).as_str()) ); } @@ -713,29 +717,35 @@ fn test_ancestors_chain_walk() { #[test] fn test_instance_try_new_validates_chain_match() { let type_schema = Arc::new(make_type_schema( - "gts.acme.core.events.user.v1~", + gts_id!("acme.core.events.user.v1~"), json!({}), None, )); let inst = GtsInstance::try_new( - GtsInstanceId::new("gts.acme.core.events.user.v1~", "acme.core.instances.u1.v1"), + GtsInstanceId::new( + gts_id!("acme.core.events.user.v1~"), + "acme.core.instances.u1.v1", + ), json!({ "id": "acme.core.instances.u1.v1" }), None, type_schema, ) .unwrap(); - assert_eq!(inst.type_id().as_ref(), "gts.acme.core.events.user.v1~"); + assert_eq!( + inst.type_id().as_ref(), + gts_id!("acme.core.events.user.v1~") + ); } #[test] fn test_instance_try_new_rejects_mismatched_type_schema() { let type_schema = Arc::new(make_type_schema( - "gts.acme.other.pkg.type.v1~", + gts_id!("acme.other.pkg.type.v1~"), json!({}), None, )); let err = GtsInstance::try_new( - GtsInstanceId::new("gts.acme.core.events.user.v1~", "u1"), + GtsInstanceId::new(gts_id!("acme.core.events.user.v1~"), "u1"), json!({}), None, type_schema, @@ -747,12 +757,12 @@ fn test_instance_try_new_rejects_mismatched_type_schema() { #[test] fn test_instance_rejects_type_id() { let type_schema = Arc::new(make_type_schema( - "gts.acme.core.users.user.v1~", + gts_id!("acme.core.users.user.v1~"), json!({}), None, )); let err = GtsInstance::try_new( - GtsInstanceId::new("gts.acme.core.users.user.v1~", ""), + GtsInstanceId::new(gts_id!("acme.core.users.user.v1~"), ""), json!({}), None, type_schema, @@ -766,7 +776,8 @@ fn test_type_schema_query_builder() { let empty = TypeSchemaQuery::new(); assert!(empty.is_empty()); - let q = TypeSchemaQuery::new().with_pattern("gts.acme.*"); + let pattern = format!("{GTS_ID_PREFIX}acme.*"); + let q = TypeSchemaQuery::new().with_pattern(pattern.as_str()); assert!(!q.is_empty()); - assert_eq!(q.pattern.as_deref(), Some("gts.acme.*")); + assert_eq!(q.pattern.as_deref(), Some(pattern.as_str())); } diff --git a/gears/system/types-registry/types-registry-sdk/src/testing.rs b/gears/system/types-registry/types-registry-sdk/src/testing.rs index ab37c7a86..f8a5a61ad 100644 --- a/gears/system/types-registry/types-registry-sdk/src/testing.rs +++ b/gears/system/types-registry/types-registry-sdk/src/testing.rs @@ -31,7 +31,7 @@ use crate::models::{ GtsInstance, GtsTypeId, GtsTypeSchema, InstanceQuery, RegisterResult, TypeSchemaQuery, is_type_schema_id, }; -use gts::{GtsID, GtsInstanceId}; +use gts::{GtsId, GtsInstanceId}; /// Builds the `InvalidArgument` canonical error the registry returns for a /// malformed / kind-mismatched GTS id (reason [`field::INVALID_GTS_ID`]), @@ -186,7 +186,7 @@ impl TypesRegistryClient for MockTypesRegistryClient { if !is_type_schema_id(type_id) { return Err(invalid_gts_id(format!("{type_id} does not end with `~`"))); } - GtsID::new(type_id).map_err(|e| invalid_gts_id(format!("{e}")))?; + GtsId::try_new(type_id).map_err(|e| invalid_gts_id(format!("{e}")))?; self.type_schemas .iter() .find(|s| s.type_id == type_id) @@ -261,7 +261,7 @@ impl TypesRegistryClient for MockTypesRegistryClient { "{id} ends with `~` (looks like a type-schema id)", ))); } - GtsID::new(id).map_err(|e| invalid_gts_id(format!("{e}")))?; + GtsId::try_new(id).map_err(|e| invalid_gts_id(format!("{e}")))?; self.instances .iter() .find(|e| e.id == id) diff --git a/gears/system/types-registry/types-registry/src/api/rest/dto.rs b/gears/system/types-registry/types-registry/src/api/rest/dto.rs index 1e6e28474..450eedae9 100644 --- a/gears/system/types-registry/types-registry/src/api/rest/dto.rs +++ b/gears/system/types-registry/types-registry/src/api/rest/dto.rs @@ -26,11 +26,11 @@ pub struct GtsIdSegmentDto { impl From<&GtsIdSegment> for GtsIdSegmentDto { fn from(segment: &GtsIdSegment) -> Self { Self { - vendor: segment.vendor.clone(), - package: segment.package.clone(), - namespace: segment.namespace.clone(), - type_name: segment.type_name.clone(), - ver_major: segment.ver_major, + vendor: segment.vendor().to_owned(), + package: segment.package().to_owned(), + namespace: segment.namespace().to_owned(), + type_name: segment.type_name().to_owned(), + ver_major: segment.ver_major(), } } } @@ -214,13 +214,23 @@ pub struct ListEntitiesResponse { mod tests { use super::*; use gts::GtsIdSegment; + use toolkit_gts::{GTS_ID_PREFIX, gts_id}; + + fn seg(full_id: &str, idx: usize) -> GtsIdSegment { + gts::GtsId::try_new(full_id) + .unwrap_or_else(|e| panic!("invalid GTS id `{full_id}`: {e}")) + .into_segments() + .remove(idx) + } #[test] fn test_gts_entity_dto_from_entity() { - let segment = GtsIdSegment::new(0, 0, "acme.core.events.user_created.v1~").unwrap(); + const TYPE_ID: &str = gts_id!("acme.core.events.user_created.v1~"); + + let segment = seg(TYPE_ID, 0); let entity = GtsEntity::new( Uuid::nil(), - "gts.acme.core.events.user_created.v1~", + TYPE_ID, vec![segment], true, // is_schema serde_json::json!({"type": "object"}), @@ -228,7 +238,7 @@ mod tests { ); let dto: GtsEntityDto = entity.into(); - assert_eq!(dto.gts_id, "gts.acme.core.events.user_created.v1~"); + assert_eq!(dto.gts_id, TYPE_ID); assert!(dto.is_schema); assert_eq!(dto.segments.len(), 1); assert_eq!(dto.segments[0].vendor, "acme"); @@ -241,9 +251,12 @@ mod tests { #[test] fn test_gts_entity_dto_instance() { + const INSTANCE_ID: &str = + gts_id!("acme.core.events.user_created.v1~acme.core.instances.instance1.v1"); + let entity = GtsEntity::new( Uuid::nil(), - "gts.acme.core.events.user_created.v1~acme.core.instances.instance1.v1", + INSTANCE_ID, vec![], false, // is_schema serde_json::json!({"data": "value"}), @@ -258,11 +271,13 @@ mod tests { #[test] fn test_gts_entity_dto_with_multiple_segments() { - let segment1 = GtsIdSegment::new(0, 0, "acme.core.models.user.v1~").unwrap(); - let segment2 = GtsIdSegment::new(1, 30, "acme.core.instances.user1.v1").unwrap(); + const INSTANCE_ID: &str = gts_id!("acme.core.models.user.v1~acme.core.instances.user1.v1"); + + let segment1 = seg(INSTANCE_ID, 0); + let segment2 = seg(INSTANCE_ID, 1); let entity = GtsEntity::new( Uuid::nil(), - "gts.acme.core.models.user.v1~acme.core.instances.user1.v1", + INSTANCE_ID, vec![segment1, segment2], false, // is_schema serde_json::json!({"userId": "user-001"}), @@ -282,12 +297,15 @@ mod tests { #[test] fn test_gts_entity_dto_with_different_vendors_in_segments() { + const INSTANCE_ID: &str = + gts_id!("acme.core.models.product.v1~globex.retail.instances.prod1.v1"); + // Instance where type and instance have different vendors - let segment1 = GtsIdSegment::new(0, 0, "acme.core.models.product.v1~").unwrap(); - let segment2 = GtsIdSegment::new(1, 32, "globex.retail.instances.prod1.v1").unwrap(); + let segment1 = seg(INSTANCE_ID, 0); + let segment2 = seg(INSTANCE_ID, 1); let entity = GtsEntity::new( Uuid::nil(), - "gts.acme.core.models.product.v1~globex.retail.instances.prod1.v1", + INSTANCE_ID, vec![segment1, segment2], false, // is_schema serde_json::json!({"productId": "prod-001"}), @@ -312,7 +330,7 @@ mod tests { #[test] fn test_gts_id_segment_dto_serialization() { - let segment = GtsIdSegment::new(0, 0, "acme.billing.invoices.invoice.v2~").unwrap(); + let segment = seg(gts_id!("acme.billing.invoices.invoice.v2~"), 0); let dto = GtsIdSegmentDto::from(&segment); let json = serde_json::to_value(&dto).unwrap(); @@ -325,11 +343,13 @@ mod tests { #[test] fn test_gts_entity_dto_segments_serialization() { - let segment1 = GtsIdSegment::new(0, 0, "fabrikam.pkg1.ns1.type1.v1~").unwrap(); - let segment2 = GtsIdSegment::new(1, 28, "contoso.pkg2.ns2.inst1.v2").unwrap(); + const INSTANCE_ID: &str = gts_id!("fabrikam.pkg1.ns1.type1.v1~contoso.pkg2.ns2.inst1.v2"); + + let segment1 = seg(INSTANCE_ID, 0); + let segment2 = seg(INSTANCE_ID, 1); let entity = GtsEntity::new( Uuid::nil(), - "gts.fabrikam.pkg1.ns1.type1.v1~contoso.pkg2.ns2.inst1.v2", + INSTANCE_ID, vec![segment1, segment2], false, // is_schema serde_json::json!({}), @@ -347,16 +367,17 @@ mod tests { #[test] fn test_list_entities_query_to_list_query() { + let pattern = format!("{GTS_ID_PREFIX}acme.*"); let dto = ListEntitiesQuery { #[allow(unknown_lints)] #[allow(de0901_gts_string_pattern)] - pattern: Some("gts.acme.*".to_owned()), + pattern: Some(pattern.clone()), is_schema: Some(true), ..ListEntitiesQuery::default() }; let query = dto.to_list_query(); - assert_eq!(query.pattern, Some("gts.acme.*".to_owned())); + assert_eq!(query.pattern, Some(pattern)); assert_eq!(query.is_type, Some(true)); } diff --git a/gears/system/types-registry/types-registry/src/api/rest/error.rs b/gears/system/types-registry/types-registry/src/api/rest/error.rs index 00879a5d4..94328467a 100644 --- a/gears/system/types-registry/types-registry/src/api/rest/error.rs +++ b/gears/system/types-registry/types-registry/src/api/rest/error.rs @@ -5,7 +5,7 @@ use types_registry_sdk::{field, precondition}; use crate::domain::error::DomainError; -#[resource_error("gts.cf.types_registry.registry.type.v1~")] +#[resource_error(gts_id!("cf.types_registry.registry.type.v1~"))] pub struct TypeRegistryError; impl From for CanonicalError { @@ -80,6 +80,7 @@ impl From for CanonicalError { mod tests { use super::*; use toolkit_canonical_errors::Problem; + use toolkit_gts::{GTS_ID_PREFIX, gts_id}; fn problem_from(err: DomainError) -> Problem { // Construct the wire `Problem` the same way the canonical error @@ -90,7 +91,9 @@ mod tests { #[test] fn test_domain_error_to_problem_not_found_by_id() { - let problem = problem_from(DomainError::not_found_by_id("gts.cf.core.events.test.v1~")); + let problem = problem_from(DomainError::not_found_by_id(gts_id!( + "cf.core.events.test.v1~" + ))); assert_eq!(problem.status, 404); // `instance` is filled by the canonical error middleware on the way // out — at the unit-test level no middleware is in scope. @@ -98,7 +101,7 @@ mod tests { assert!( problem .detail - .contains("GTS ID: gts.cf.core.events.test.v1~"), + .contains(&format!("GTS ID: {}", gts_id!("cf.core.events.test.v1~"))), "expected GTS-id-keyed detail, got {:?}", problem.detail, ); @@ -119,7 +122,9 @@ mod tests { #[test] fn test_domain_error_to_problem_already_exists() { - let problem = problem_from(DomainError::already_exists("gts.cf.core.events.test.v1~")); + let problem = problem_from(DomainError::already_exists(gts_id!( + "cf.core.events.test.v1~" + ))); assert_eq!(problem.status, 409); } @@ -145,9 +150,9 @@ mod tests { fn test_domain_error_to_problem_ready_commit_failed() { use crate::domain::error::ValidationError; let problem = problem_from(DomainError::ReadyCommitFailed(vec![ - ValidationError::new("gts.test1~", "error1"), - ValidationError::new("gts.test2~", "error2"), - ValidationError::new("gts.test3~", "error3"), + ValidationError::new(format!("{GTS_ID_PREFIX}test1~"), "error1"), + ValidationError::new(format!("{GTS_ID_PREFIX}test2~"), "error2"), + ValidationError::new(format!("{GTS_ID_PREFIX}test3~"), "error3"), ])); // ReadyCommitFailed is only produced by post_init lifecycle and // never reaches a REST response; map opaquely to internal. diff --git a/gears/system/types-registry/types-registry/src/api/rest/handlers.rs b/gears/system/types-registry/types-registry/src/api/rest/handlers.rs index 9d09edf93..0586d139b 100644 --- a/gears/system/types-registry/types-registry/src/api/rest/handlers.rs +++ b/gears/system/types-registry/types-registry/src/api/rest/handlers.rs @@ -105,6 +105,7 @@ mod tests { use crate::infra::InMemoryGtsRepository; use gts::GtsConfig; use serde_json::json; + use toolkit_gts::{gts_id, gts_uri}; const JSON_SCHEMA_DRAFT_07: &str = "http://json-schema.org/draft-07/schema#"; @@ -127,7 +128,7 @@ mod tests { let req = RegisterEntitiesRequest { entities: vec![json!({ - "$id": "gts://gts.acme.core.events.user_created.v1~", + "$id": gts_uri!("acme.core.events.user_created.v1~"), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object" })], @@ -154,7 +155,7 @@ mod tests { let result = get_entity( Extension(service), - Path("gts.acme.core.events.user_created.v1~".to_owned()), + Path(gts_id!("acme.core.events.user_created.v1~").to_owned()), ) .await; assert!(result.is_err()); @@ -167,7 +168,7 @@ mod tests { let req = RegisterEntitiesRequest { entities: vec![json!({ - "$id": "gts://gts.acme.core.events.user_created.v1~", + "$id": gts_uri!("acme.core.events.user_created.v1~"), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object" })], @@ -190,12 +191,12 @@ mod tests { // Register entities via internal API (before ready) _ = service.register(vec![ json!({ - "$id": "gts://gts.acme.core.events.user_created.v1~", + "$id": gts_uri!("acme.core.events.user_created.v1~"), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object" }), json!({ - "$id": "gts://gts.globex.core.events.order_placed.v1~", + "$id": gts_uri!("globex.core.events.order_placed.v1~"), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object" }), @@ -216,7 +217,7 @@ mod tests { // Register entity via internal API (before ready) _ = service.register(vec![json!({ - "$id": "gts://gts.acme.core.events.user_created.v1~", + "$id": gts_uri!("acme.core.events.user_created.v1~"), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object" })]); @@ -224,13 +225,13 @@ mod tests { let result = get_entity( Extension(service), - Path("gts.acme.core.events.user_created.v1~".to_owned()), + Path(gts_id!("acme.core.events.user_created.v1~").to_owned()), ) .await; assert!(result.is_ok()); let Json(entity) = result.unwrap(); - assert_eq!(entity.gts_id, "gts.acme.core.events.user_created.v1~"); + assert_eq!(entity.gts_id, gts_id!("acme.core.events.user_created.v1~")); } #[tokio::test] @@ -240,7 +241,7 @@ mod tests { let result = get_entity( Extension(service), - Path("gts.fabrikam.pkg.ns.type.v1~".to_owned()), + Path(gts_id!("fabrikam.pkg.ns.type.v1~").to_owned()), ) .await; assert!(result.is_err()); diff --git a/gears/system/types-registry/types-registry/src/api/rest/routes.rs b/gears/system/types-registry/types-registry/src/api/rest/routes.rs index d18a2f736..b2585feae 100644 --- a/gears/system/types-registry/types-registry/src/api/rest/routes.rs +++ b/gears/system/types-registry/types-registry/src/api/rest/routes.rs @@ -5,7 +5,9 @@ use std::sync::Arc; use axum::{Extension, Router}; use toolkit::api::OpenApiRegistry; use toolkit::api::canonical_prelude::StatusCode; -use toolkit::api::operation_builder::{LicenseFeature, OperationBuilder}; +use toolkit::api::operation_builder::{ + CORE_GLOBAL_BASE_LICENSE_FEATURE, LicenseFeature, OperationBuilder, +}; use super::dto::{ GtsEntityDto, ListEntitiesResponse, RegisterEntitiesRequest, RegisterEntitiesResponse, @@ -19,7 +21,7 @@ struct License; impl AsRef for License { fn as_ref(&self) -> &'static str { - "gts.cf.core.lic.feat.v1~cf.core.global.base.v1" + CORE_GLOBAL_BASE_LICENSE_FEATURE } } diff --git a/gears/system/types-registry/types-registry/src/domain/error.rs b/gears/system/types-registry/types-registry/src/domain/error.rs index 4ef17b2a7..abf2f60f0 100644 --- a/gears/system/types-registry/types-registry/src/domain/error.rs +++ b/gears/system/types-registry/types-registry/src/domain/error.rs @@ -189,13 +189,14 @@ impl DomainError { #[cfg(test)] mod tests { use super::*; + use toolkit_gts::{GTS_ID_PREFIX, gts_id}; #[test] fn test_error_constructors() { let err = DomainError::invalid_gts_id("missing vendor"); assert!(matches!(err, DomainError::InvalidGtsId(_))); - let err = DomainError::not_found_by_id("gts.acme.core.events.test.v1~"); + let err = DomainError::not_found_by_id(gts_id!("acme.core.events.test.v1~")); assert!(matches!( err, DomainError::NotFound { @@ -213,7 +214,7 @@ mod tests { } )); - let err = DomainError::already_exists("gts.acme.core.events.test.v1~"); + let err = DomainError::already_exists(gts_id!("acme.core.events.test.v1~")); assert!(matches!(err, DomainError::AlreadyExists(_))); let err = DomainError::validation_failed("schema invalid"); @@ -225,10 +226,13 @@ mod tests { let err = DomainError::InvalidGtsId("bad format".to_owned()); assert_eq!(err.to_string(), "Invalid GTS ID: bad format"); - let err = DomainError::not_found_by_id("gts.cf.core.events.test.v1~"); + let err = DomainError::not_found_by_id(gts_id!("cf.core.events.test.v1~")); assert_eq!( err.to_string(), - "Entity not found (GTS ID): gts.cf.core.events.test.v1~" + format!( + "Entity not found (GTS ID): {}", + gts_id!("cf.core.events.test.v1~") + ) ); let err = DomainError::not_found_by_uuid(uuid::Uuid::nil()); @@ -237,10 +241,13 @@ mod tests { "Entity not found (UUID): 00000000-0000-0000-0000-000000000000" ); - let err = DomainError::AlreadyExists("gts.cf.core.events.test.v1~".to_owned()); + let err = DomainError::AlreadyExists(gts_id!("cf.core.events.test.v1~").to_owned()); assert_eq!( err.to_string(), - "Entity already exists: gts.cf.core.events.test.v1~" + format!( + "Entity already exists: {}", + gts_id!("cf.core.events.test.v1~") + ) ); let err = DomainError::ValidationFailed("schema invalid".to_owned()); @@ -250,9 +257,9 @@ mod tests { assert_eq!(err.to_string(), "Not in ready mode"); let err = DomainError::ReadyCommitFailed(vec![ - ValidationError::new("gts.test1~", "error1"), - ValidationError::new("gts.test2~", "error2"), - ValidationError::new("gts.test3~", "error3"), + ValidationError::new(format!("{GTS_ID_PREFIX}test1~"), "error1"), + ValidationError::new(format!("{GTS_ID_PREFIX}test2~"), "error2"), + ValidationError::new(format!("{GTS_ID_PREFIX}test3~"), "error3"), ]); assert_eq!(err.to_string(), "Ready commit failed with 3 errors"); } diff --git a/gears/system/types-registry/types-registry/src/domain/local_client_tests.rs b/gears/system/types-registry/types-registry/src/domain/local_client_tests.rs index 57a7f9f22..198a21404 100644 --- a/gears/system/types-registry/types-registry/src/domain/local_client_tests.rs +++ b/gears/system/types-registry/types-registry/src/domain/local_client_tests.rs @@ -11,6 +11,8 @@ use gts::GtsConfig; use serde_json::json; use std::time::Duration; use toolkit_canonical_errors::InvalidArgument; +use toolkit_gts::gts_id; +use toolkit_gts::gts_uri; const JSON_SCHEMA_DRAFT_07: &str = "https://json-schema.org/draft-07/schema#"; @@ -60,7 +62,7 @@ fn create_client() -> TypesRegistryLocalClient { async fn test_register_and_get_type_schema() { let client = create_client(); let entity = json!({ - "$id": "gts://gts.acme.core.events.user_created.v1~", + "$id": gts_uri!("acme.core.events.user_created.v1~"), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object", "properties": { "userId": { "type": "string" } } @@ -72,12 +74,12 @@ async fn test_register_and_get_type_schema() { client.service.switch_to_ready().unwrap(); let retrieved = client - .get_type_schema("gts.acme.core.events.user_created.v1~") + .get_type_schema(gts_id!("acme.core.events.user_created.v1~")) .await .unwrap(); assert_eq!( retrieved.type_id.as_ref(), - "gts.acme.core.events.user_created.v1~" + gts_id!("acme.core.events.user_created.v1~") ); assert!(retrieved.parent.is_none()); } @@ -85,20 +87,20 @@ async fn test_register_and_get_type_schema() { #[tokio::test] async fn test_get_type_schema_resolves_parent_chain() { let client = create_client(); - let base_id = "gts.acme.core.events.base.v1~"; - let derived_id = "gts.acme.core.events.base.v1~acme.core.events.derived.v1.0~"; + let base_id = gts_id!("acme.core.events.base.v1~"); + let derived_id = gts_id!("acme.core.events.base.v1~acme.core.events.derived.v1.0~"); let base = json!({ - "$id": format!("gts://{base_id}"), + "$id": gts_uri!(base_id), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object", "properties": { "id": { "type": "string" } } }); let derived = json!({ - "$id": format!("gts://{derived_id}"), + "$id": gts_uri!(derived_id), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object", "allOf": [ - { "$ref": format!("gts://{base_id}") }, + { "$ref": gts_uri!(base_id) }, { "properties": { "name": { "type": "string" } } } ] }); @@ -117,26 +119,26 @@ async fn test_get_type_schema_resolves_parent_chain() { #[tokio::test] async fn test_type_schema_cache_dedups_parents() { let client = create_client(); - let base_id = "gts.acme.core.events.base.v1~"; - let d1_id = "gts.acme.core.events.base.v1~acme.core.events.d1.v1.0~"; - let d2_id = "gts.acme.core.events.base.v1~acme.core.events.d2.v1.0~"; + let base_id = gts_id!("acme.core.events.base.v1~"); + let d1_id = gts_id!("acme.core.events.base.v1~acme.core.events.d1.v1.0~"); + let d2_id = gts_id!("acme.core.events.base.v1~acme.core.events.d2.v1.0~"); let base = json!({ - "$id": format!("gts://{base_id}"), + "$id": gts_uri!(base_id), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object", "properties": { "id": { "type": "string" } } }); let d1 = json!({ - "$id": format!("gts://{d1_id}"), + "$id": gts_uri!(d1_id), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object", - "allOf": [{ "$ref": format!("gts://{base_id}") }] + "allOf": [{ "$ref": gts_uri!(base_id) }] }); let d2 = json!({ - "$id": format!("gts://{d2_id}"), + "$id": gts_uri!(d2_id), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object", - "allOf": [{ "$ref": format!("gts://{base_id}") }] + "allOf": [{ "$ref": gts_uri!(base_id) }] }); client.register(vec![base, d1, d2]).await.unwrap(); client.service.switch_to_ready().unwrap(); @@ -152,25 +154,30 @@ async fn test_type_schema_cache_dedups_parents() { async fn test_get_instance_carries_type_schema_arc() { let client = create_client(); let schema = json!({ - "$id": "gts://gts.acme.core.events.user.v1~", + "$id": gts_uri!("acme.core.events.user.v1~"), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object" }); let instance = json!({ - "id": "gts.acme.core.events.user.v1~acme.core.instances.u1.v1", - "type": "gts.acme.core.events.user.v1~" + "id": gts_id!("acme.core.events.user.v1~acme.core.instances.u1.v1"), + "type": gts_id!("acme.core.events.user.v1~") }); client.register(vec![schema, instance]).await.unwrap(); client.service.switch_to_ready().unwrap(); let inst = client - .get_instance("gts.acme.core.events.user.v1~acme.core.instances.u1.v1") + .get_instance(gts_id!( + "acme.core.events.user.v1~acme.core.instances.u1.v1" + )) .await .unwrap(); - assert_eq!(inst.type_id().as_ref(), "gts.acme.core.events.user.v1~"); + assert_eq!( + inst.type_id().as_ref(), + gts_id!("acme.core.events.user.v1~") + ); assert_eq!( inst.type_schema.type_id.as_ref(), - "gts.acme.core.events.user.v1~" + gts_id!("acme.core.events.user.v1~") ); } @@ -180,18 +187,18 @@ async fn test_instance_cache_returns_same_value() { // returns an equal value. let client = create_client(); let schema = json!({ - "$id": "gts://gts.acme.core.events.user.v1~", + "$id": gts_uri!("acme.core.events.user.v1~"), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object" }); let instance = json!({ - "id": "gts.acme.core.events.user.v1~acme.core.instances.u1.v1", - "type": "gts.acme.core.events.user.v1~" + "id": gts_id!("acme.core.events.user.v1~acme.core.instances.u1.v1"), + "type": gts_id!("acme.core.events.user.v1~") }); client.register(vec![schema, instance]).await.unwrap(); client.service.switch_to_ready().unwrap(); - let id = "gts.acme.core.events.user.v1~acme.core.instances.u1.v1"; + let id = gts_id!("acme.core.events.user.v1~acme.core.instances.u1.v1"); let i1 = client.get_instance(id).await.unwrap(); let i2 = client.get_instance(id).await.unwrap(); assert_eq!(i1.id, i2.id); @@ -204,7 +211,7 @@ async fn test_instance_cache_returns_same_value() { async fn test_clear_caches_drops_type_schema_arcs() { let client = create_client(); let schema = json!({ - "$id": "gts://gts.acme.core.events.user.v1~", + "$id": gts_uri!("acme.core.events.user.v1~"), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object" }); @@ -212,7 +219,7 @@ async fn test_clear_caches_drops_type_schema_arcs() { client.service.switch_to_ready().unwrap(); let s1 = client - .get_type_schema("gts.acme.core.events.user.v1~") + .get_type_schema(gts_id!("acme.core.events.user.v1~")) .await .unwrap(); // After the get, the cache must hold the entry — both LRU and the @@ -227,7 +234,7 @@ async fn test_clear_caches_drops_type_schema_arcs() { // Subsequent get rebuilds and repopulates the cache. let s2 = client - .get_type_schema("gts.acme.core.events.user.v1~") + .get_type_schema(gts_id!("acme.core.events.user.v1~")) .await .unwrap(); assert_eq!(s2.type_id, s1.type_id); @@ -238,12 +245,12 @@ async fn test_clear_caches_drops_type_schema_arcs() { async fn test_invalidate_type_schema_drops_only_one_entry() { let client = create_client(); let s1 = json!({ - "$id": "gts://gts.acme.core.events.a.v1~", + "$id": gts_uri!("acme.core.events.a.v1~"), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object" }); let s2 = json!({ - "$id": "gts://gts.acme.core.events.b.v1~", + "$id": gts_uri!("acme.core.events.b.v1~"), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object" }); @@ -251,16 +258,16 @@ async fn test_invalidate_type_schema_drops_only_one_entry() { client.service.switch_to_ready().unwrap(); client - .get_type_schema("gts.acme.core.events.a.v1~") + .get_type_schema(gts_id!("acme.core.events.a.v1~")) .await .unwrap(); client - .get_type_schema("gts.acme.core.events.b.v1~") + .get_type_schema(gts_id!("acme.core.events.b.v1~")) .await .unwrap(); assert_eq!(client.type_schemas.len(), 2); - client.invalidate_type_schema("gts.acme.core.events.a.v1~"); + client.invalidate_type_schema(gts_id!("acme.core.events.a.v1~")); assert_eq!(client.type_schemas.len(), 1); } @@ -279,14 +286,14 @@ async fn test_custom_cache_configs_apply() { CacheConfig::instances().with_capacity(8), ); let s = json!({ - "$id": "gts://gts.acme.core.events.user.v1~", + "$id": gts_uri!("acme.core.events.user.v1~"), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object" }); client.register(vec![s]).await.unwrap(); client.service.switch_to_ready().unwrap(); let _ = client - .get_type_schema("gts.acme.core.events.user.v1~") + .get_type_schema(gts_id!("acme.core.events.user.v1~")) .await .unwrap(); assert_eq!(client.type_schemas.len(), 1); @@ -300,7 +307,7 @@ async fn test_custom_cache_configs_apply() { assert!( client .type_schemas - .get("gts.acme.core.events.user.v1~") + .get(gts_id!("acme.core.events.user.v1~")) .is_none(), "TTL did not evict expired entry" ); @@ -308,7 +315,7 @@ async fn test_custom_cache_configs_apply() { // Subsequent get rebuilds and repopulates the cache. let _ = client - .get_type_schema("gts.acme.core.events.user.v1~") + .get_type_schema(gts_id!("acme.core.events.user.v1~")) .await .unwrap(); assert_eq!(client.type_schemas.len(), 1); @@ -318,19 +325,21 @@ async fn test_custom_cache_configs_apply() { async fn test_get_type_schema_rejects_instance() { let client = create_client(); let schema = json!({ - "$id": "gts://gts.acme.core.events.user.v1~", + "$id": gts_uri!("acme.core.events.user.v1~"), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object" }); let instance = json!({ - "id": "gts.acme.core.events.user.v1~acme.core.instances.u1.v1", - "type": "gts.acme.core.events.user.v1~" + "id": gts_id!("acme.core.events.user.v1~acme.core.instances.u1.v1"), + "type": gts_id!("acme.core.events.user.v1~") }); client.register(vec![schema, instance]).await.unwrap(); client.service.switch_to_ready().unwrap(); let err = client - .get_type_schema("gts.acme.core.events.user.v1~acme.core.instances.u1.v1") + .get_type_schema(gts_id!( + "acme.core.events.user.v1~acme.core.instances.u1.v1" + )) .await .unwrap_err(); assert!(is_invalid_gts_id(&err)); @@ -340,8 +349,8 @@ async fn test_get_type_schema_rejects_instance() { async fn test_register_type_schemas_rejects_instance_input() { let client = create_client(); let instance = json!({ - "id": "gts.acme.core.events.user.v1~acme.core.instances.u1.v1", - "type": "gts.acme.core.events.user.v1~" + "id": gts_id!("acme.core.events.user.v1~acme.core.instances.u1.v1"), + "type": gts_id!("acme.core.events.user.v1~") }); let results = client.register_type_schemas(vec![instance]).await.unwrap(); assert_eq!(results.len(), 1); @@ -356,18 +365,18 @@ async fn test_register_type_schemas_unsorted_batch_succeeds() { // Batch order [derived, base] is reordered by sort to [base, derived] // so the parent registers before its child within a single call. let client = create_client(); - let base_id = "gts.acme.core.events.base.v1~"; - let derived_id = "gts.acme.core.events.base.v1~acme.core.events.derived.v1.0~"; + let base_id = gts_id!("acme.core.events.base.v1~"); + let derived_id = gts_id!("acme.core.events.base.v1~acme.core.events.derived.v1.0~"); let base = json!({ - "$id": format!("gts://{base_id}"), + "$id": gts_uri!(base_id), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object", }); let derived = json!({ - "$id": format!("gts://{derived_id}"), + "$id": gts_uri!(derived_id), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object", - "allOf": [{ "$ref": format!("gts://{base_id}") }] + "allOf": [{ "$ref": gts_uri!(base_id) }] }); // Pass derived first; sort should put base first. let results = client @@ -387,9 +396,9 @@ async fn test_register_type_schemas_orphan_derived_in_ready_fails() { let client = create_client(); client.service.switch_to_ready().unwrap(); - let derived_id = "gts.acme.core.events.base.v1~acme.core.events.derived.v1.0~"; + let derived_id = gts_id!("acme.core.events.base.v1~acme.core.events.derived.v1.0~"); let derived = json!({ - "$id": format!("gts://{derived_id}"), + "$id": gts_uri!(derived_id), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object", }); @@ -411,8 +420,8 @@ async fn test_register_instances_orphan_in_ready_fails() { client.service.switch_to_ready().unwrap(); let instance = json!({ - "id": "gts.acme.core.events.user.v1~acme.core.instances.u1.v1", - "type": "gts.acme.core.events.user.v1~" + "id": gts_id!("acme.core.events.user.v1~acme.core.instances.u1.v1"), + "type": gts_id!("acme.core.events.user.v1~") }); let results = client.register_instances(vec![instance]).await.unwrap(); assert_eq!(results.len(), 1); @@ -428,13 +437,13 @@ async fn test_register_instances_orphan_in_ready_fails() { async fn test_list_type_schemas_and_instances_filtered() { let client = create_client(); let schema = json!({ - "$id": "gts://gts.acme.core.events.user.v1~", + "$id": gts_uri!("acme.core.events.user.v1~"), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object" }); let instance = json!({ - "id": "gts.acme.core.events.user.v1~acme.core.instances.u1.v1", - "type": "gts.acme.core.events.user.v1~" + "id": gts_id!("acme.core.events.user.v1~acme.core.instances.u1.v1"), + "type": gts_id!("acme.core.events.user.v1~") }); client.register(vec![schema, instance]).await.unwrap(); client.service.switch_to_ready().unwrap(); @@ -458,7 +467,7 @@ async fn test_list_type_schemas_and_instances_filtered() { async fn test_get_type_schema_by_uuid() { let client = create_client(); let schema = json!({ - "$id": "gts://gts.acme.core.events.user.v1~", + "$id": gts_uri!("acme.core.events.user.v1~"), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object" }); @@ -471,7 +480,10 @@ async fn test_get_type_schema_by_uuid() { .unwrap(); let uuid = listed[0].type_uuid; let by_uuid = client.get_type_schema_by_uuid(uuid).await.unwrap(); - assert_eq!(by_uuid.type_id.as_ref(), "gts.acme.core.events.user.v1~"); + assert_eq!( + by_uuid.type_id.as_ref(), + gts_id!("acme.core.events.user.v1~") + ); let unknown = client .get_type_schema_by_uuid(Uuid::nil()) @@ -487,7 +499,7 @@ async fn test_uuid_index_populated_by_get() { // can take the fast path. let client = create_client(); let schema = json!({ - "$id": "gts://gts.acme.core.events.user.v1~", + "$id": gts_uri!("acme.core.events.user.v1~"), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object" }); @@ -497,7 +509,7 @@ async fn test_uuid_index_populated_by_get() { // First call by gts_id — populates type_schema cache, which writes the // uuid → gts_id mapping into the cache's internal reverse index. let by_id = client - .get_type_schema("gts.acme.core.events.user.v1~") + .get_type_schema(gts_id!("acme.core.events.user.v1~")) .await .unwrap(); assert!(client.type_schemas.get_by_uuid(by_id.type_uuid).is_some()); @@ -516,19 +528,19 @@ async fn test_invalidate_type_schema_cascades_to_dependents() { // schemas whose chain references the base. Cache walks the // resolved Arc chain via `ancestors()`. let client = create_client(); - let base_id = "gts.acme.core.events.base.v1~"; - let derived_id = "gts.acme.core.events.base.v1~acme.core.events.derived.v1.0~"; + let base_id = gts_id!("acme.core.events.base.v1~"); + let derived_id = gts_id!("acme.core.events.base.v1~acme.core.events.derived.v1.0~"); let base = json!({ - "$id": format!("gts://{base_id}"), + "$id": gts_uri!(base_id), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object", }); let derived = json!({ - "$id": format!("gts://{derived_id}"), + "$id": gts_uri!(derived_id), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object", "allOf": [ - { "$ref": format!("gts://{base_id}") }, + { "$ref": gts_uri!(base_id) }, { "properties": { "extra": { "type": "string" } } } ], }); @@ -549,14 +561,14 @@ async fn test_invalidate_type_schema_cascades_to_dependents() { async fn test_clear_caches_resets_uuid_index() { let client = create_client(); let schema = json!({ - "$id": "gts://gts.acme.core.events.user.v1~", + "$id": gts_uri!("acme.core.events.user.v1~"), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object" }); client.register(vec![schema]).await.unwrap(); client.service.switch_to_ready().unwrap(); let warmed = client - .get_type_schema("gts.acme.core.events.user.v1~") + .get_type_schema(gts_id!("acme.core.events.user.v1~")) .await .unwrap(); assert!(client.type_schemas.get_by_uuid(warmed.type_uuid).is_some()); @@ -572,18 +584,18 @@ async fn test_get_type_schemas_returns_keyed_map() { let client = create_client(); client .register(vec![ - json!({"$id": "gts://gts.acme.core.events.alpha.v1~", "$schema": JSON_SCHEMA_DRAFT_07, "type": "object"}), - json!({"$id": "gts://gts.acme.core.events.beta.v1~", "$schema": JSON_SCHEMA_DRAFT_07, "type": "object"}), - json!({"$id": "gts://gts.acme.core.events.gamma.v1~", "$schema": JSON_SCHEMA_DRAFT_07, "type": "object"}), + json!({"$id": gts_uri!("acme.core.events.alpha.v1~"), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object"}), + json!({"$id": gts_uri!("acme.core.events.beta.v1~"), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object"}), + json!({"$id": gts_uri!("acme.core.events.gamma.v1~"), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object"}), ]) .await .unwrap(); client.service.switch_to_ready().unwrap(); let ids = vec![ - "gts.acme.core.events.gamma.v1~".to_owned(), - "gts.acme.core.events.alpha.v1~".to_owned(), - "gts.acme.core.events.beta.v1~".to_owned(), + gts_id!("acme.core.events.gamma.v1~").to_owned(), + gts_id!("acme.core.events.alpha.v1~").to_owned(), + gts_id!("acme.core.events.beta.v1~").to_owned(), ]; let results = client.get_type_schemas(ids.clone()).await; assert_eq!(results.len(), 3); @@ -604,16 +616,16 @@ async fn test_get_type_schemas_partial_failures() { let client = create_client(); client .register(vec![ - json!({"$id": "gts://gts.acme.core.events.alpha.v1~", "$schema": JSON_SCHEMA_DRAFT_07, "type": "object"}), - json!({"$id": "gts://gts.acme.core.events.gamma.v1~", "$schema": JSON_SCHEMA_DRAFT_07, "type": "object"}), + json!({"$id": gts_uri!("acme.core.events.alpha.v1~"), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object"}), + json!({"$id": gts_uri!("acme.core.events.gamma.v1~"), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object"}), ]) .await .unwrap(); client.service.switch_to_ready().unwrap(); - let alpha = "gts.acme.core.events.alpha.v1~"; - let missing = "gts.acme.core.events.missing.v1~"; - let gamma = "gts.acme.core.events.gamma.v1~"; + let alpha = gts_id!("acme.core.events.alpha.v1~"); + let missing = gts_id!("acme.core.events.missing.v1~"); + let gamma = gts_id!("acme.core.events.gamma.v1~"); let results = client .get_type_schemas(vec![alpha.to_owned(), missing.to_owned(), gamma.to_owned()]) .await; @@ -635,7 +647,7 @@ async fn test_get_type_schemas_kind_mismatch() { // must surface as an invalid-GTS-id validation error for that single // item, not fail the whole batch. let client = create_client(); - let bad = "gts.acme.core.events.user.v1~acme.core.instances.u1.v1"; + let bad = gts_id!("acme.core.events.user.v1~acme.core.instances.u1.v1"); let results = client.get_type_schemas(vec![bad.to_owned()]).await; assert_eq!(results.len(), 1); assert!(is_invalid_gts_id( @@ -650,13 +662,13 @@ async fn test_get_type_schemas_by_uuid_warm_cache_uses_index() { let client = create_client(); client .register(vec![ - json!({"$id": "gts://gts.acme.core.events.alpha.v1~", "$schema": JSON_SCHEMA_DRAFT_07, "type": "object"}), + json!({"$id": gts_uri!("acme.core.events.alpha.v1~"), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object"}), ]) .await .unwrap(); client.service.switch_to_ready().unwrap(); let warmed = client - .get_type_schema("gts.acme.core.events.alpha.v1~") + .get_type_schema(gts_id!("acme.core.events.alpha.v1~")) .await .unwrap(); assert!(client.type_schemas.get_by_uuid(warmed.type_uuid).is_some()); @@ -681,7 +693,7 @@ async fn test_get_type_schemas_by_uuid_cold_falls_back_to_storage() { let client = create_client(); client .register(vec![ - json!({"$id": "gts://gts.acme.core.events.alpha.v1~", "$schema": JSON_SCHEMA_DRAFT_07, "type": "object"}), + json!({"$id": gts_uri!("acme.core.events.alpha.v1~"), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object"}), ]) .await .unwrap(); @@ -709,17 +721,17 @@ async fn test_get_instances_keyed_with_partial_failures() { let client = create_client(); client .register(vec![ - json!({"$id": "gts://gts.acme.core.events.user.v1~", "$schema": JSON_SCHEMA_DRAFT_07, "type": "object"}), - json!({"id": "gts.acme.core.events.user.v1~acme.core.instances.u1.v1"}), - json!({"id": "gts.acme.core.events.user.v1~acme.core.instances.u2.v1"}), + json!({"$id": gts_uri!("acme.core.events.user.v1~"), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object"}), + json!({"id": gts_id!("acme.core.events.user.v1~acme.core.instances.u1.v1")}), + json!({"id": gts_id!("acme.core.events.user.v1~acme.core.instances.u2.v1")}), ]) .await .unwrap(); client.service.switch_to_ready().unwrap(); - let u2 = "gts.acme.core.events.user.v1~acme.core.instances.u2.v1"; - let missing = "gts.acme.core.events.user.v1~acme.core.instances.missing.v1"; - let u1 = "gts.acme.core.events.user.v1~acme.core.instances.u1.v1"; + let u2 = gts_id!("acme.core.events.user.v1~acme.core.instances.u2.v1"); + let missing = gts_id!("acme.core.events.user.v1~acme.core.instances.missing.v1"); + let u1 = gts_id!("acme.core.events.user.v1~acme.core.instances.u1.v1"); let results = client .get_instances(vec![u2.to_owned(), missing.to_owned(), u1.to_owned()]) .await; @@ -747,14 +759,16 @@ async fn test_get_instances_by_uuid_warm_cache_uses_index() { let client = create_client(); client .register(vec![ - json!({"$id": "gts://gts.acme.core.events.user.v1~", "$schema": JSON_SCHEMA_DRAFT_07, "type": "object"}), - json!({"id": "gts.acme.core.events.user.v1~acme.core.instances.u1.v1"}), + json!({"$id": gts_uri!("acme.core.events.user.v1~"), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object"}), + json!({"id": gts_id!("acme.core.events.user.v1~acme.core.instances.u1.v1")}), ]) .await .unwrap(); client.service.switch_to_ready().unwrap(); let warmed = client - .get_instance("gts.acme.core.events.user.v1~acme.core.instances.u1.v1") + .get_instance(gts_id!( + "acme.core.events.user.v1~acme.core.instances.u1.v1" + )) .await .unwrap(); assert!(client.instances.get_by_uuid(warmed.uuid).is_some()); @@ -790,18 +804,18 @@ async fn test_get_batch_empty_input() { #[tokio::test] async fn test_register_preserves_caller_order() { let client = create_client(); - let base_id = "gts.acme.core.events.base.v1~"; - let derived_id = "gts.acme.core.events.base.v1~acme.core.events.derived.v1.0~"; + let base_id = gts_id!("acme.core.events.base.v1~"); + let derived_id = gts_id!("acme.core.events.base.v1~acme.core.events.derived.v1.0~"); let base = json!({ - "$id": format!("gts://{base_id}"), + "$id": gts_uri!(base_id), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object" }); let derived = json!({ - "$id": format!("gts://{derived_id}"), + "$id": gts_uri!(derived_id), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object", - "allOf": [{ "$ref": format!("gts://{base_id}") }] + "allOf": [{ "$ref": gts_uri!(base_id) }] }); // Caller passes child first, parent second. Internal sort flips this so @@ -821,18 +835,18 @@ async fn test_register_preserves_caller_order() { #[tokio::test] async fn test_register_typed_preserves_caller_order() { let client = create_client(); - let base_id = "gts.acme.core.events.base.v1~"; - let derived_id = "gts.acme.core.events.base.v1~acme.core.events.derived.v1.0~"; + let base_id = gts_id!("acme.core.events.base.v1~"); + let derived_id = gts_id!("acme.core.events.base.v1~acme.core.events.derived.v1.0~"); let base = json!({ - "$id": format!("gts://{base_id}"), + "$id": gts_uri!(base_id), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object" }); let derived = json!({ - "$id": format!("gts://{derived_id}"), + "$id": gts_uri!(derived_id), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object", - "allOf": [{ "$ref": format!("gts://{base_id}") }] + "allOf": [{ "$ref": gts_uri!(base_id) }] }); let schema_results = client @@ -845,12 +859,12 @@ async fn test_register_typed_preserves_caller_order() { // For instances, register a parent schema first, then verify caller order // for two instances of it. - let parent_id = "gts.acme.core.events.parent.v1~"; - let earlier = "gts.acme.core.events.parent.v1~acme.core.evt.a.v1"; - let later = "gts.acme.core.events.parent.v1~acme.core.evt.b.v1"; + let parent_id = gts_id!("acme.core.events.parent.v1~"); + let earlier = gts_id!("acme.core.events.parent.v1~acme.core.evt.a.v1"); + let later = gts_id!("acme.core.events.parent.v1~acme.core.evt.b.v1"); client .register(vec![json!({ - "$id": format!("gts://{parent_id}"), + "$id": gts_uri!(parent_id), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object" })]) diff --git a/gears/system/types-registry/types-registry/src/domain/model.rs b/gears/system/types-registry/types-registry/src/domain/model.rs index 023c5e3e3..63d9d2f37 100644 --- a/gears/system/types-registry/types-registry/src/domain/model.rs +++ b/gears/system/types-registry/types-registry/src/domain/model.rs @@ -78,19 +78,19 @@ impl GtsEntity { /// Returns the vendor from the primary segment. #[must_use] pub fn vendor(&self) -> Option<&str> { - self.primary_segment().map(|s| s.vendor.as_str()) + self.primary_segment().map(GtsIdSegment::vendor) } /// Returns the package from the primary segment. #[must_use] pub fn package(&self) -> Option<&str> { - self.primary_segment().map(|s| s.package.as_str()) + self.primary_segment().map(GtsIdSegment::package) } /// Returns the namespace from the primary segment. #[must_use] pub fn namespace(&self) -> Option<&str> { - self.primary_segment().map(|s| s.namespace.as_str()) + self.primary_segment().map(GtsIdSegment::namespace) } } diff --git a/gears/system/types-registry/types-registry/src/domain/service.rs b/gears/system/types-registry/types-registry/src/domain/service.rs index 14adffdc9..db389fa6c 100644 --- a/gears/system/types-registry/types-registry/src/domain/service.rs +++ b/gears/system/types-registry/types-registry/src/domain/service.rs @@ -7,6 +7,7 @@ use std::sync::Arc; use toolkit_canonical_errors::CanonicalError; +use toolkit_gts::GTS_ID_URI_PREFIX; use toolkit_macros::domain_model; use types_registry_sdk::RegisterResult; use uuid::Uuid; @@ -161,7 +162,7 @@ impl TypesRegistryService { for field in &self.config.entity_id_fields { if let Some(id) = obj.get(field.as_str()).and_then(|v| v.as_str()) { let cleaned_id = if field == "$id" { - id.strip_prefix("gts://").unwrap_or(id) + id.strip_prefix(GTS_ID_URI_PREFIX).unwrap_or(id) } else { id }; @@ -178,6 +179,7 @@ mod tests { use super::*; use serde_json::json; use std::sync::atomic::{AtomicBool, Ordering}; + use toolkit_gts::{GTS_ID_PREFIX, gts_id, gts_uri}; use toolkit_macros::domain_model; use uuid::Uuid; @@ -249,7 +251,7 @@ mod tests { fn list(&self, _query: &ListQuery) -> Result, DomainError> { Ok(vec![GtsEntity::new( Uuid::nil(), - "gts.test.pkg.ns.type.v1~".to_owned(), + gts_id!("test.pkg.ns.type.v1~").to_owned(), vec![], true, json!({}), @@ -268,8 +270,8 @@ mod tests { fn switch_to_ready(&self) -> Result<(), Vec> { if self.fail_switch { return Err(vec![ - "gts.test1~: error1".to_owned(), - "gts.test2~: error2".to_owned(), + format!("{GTS_ID_PREFIX}test1~: error1"), + format!("{GTS_ID_PREFIX}test2~: error2"), ]); } self.is_ready.store(true, Ordering::SeqCst); @@ -284,16 +286,16 @@ mod tests { crate::config::TypesRegistryConfig::default(), ); - let entity = json!({"$id": "gts://gts.acme.core.events.test.v1~"}); + let entity = json!({"$id": gts_uri!("acme.core.events.test.v1~")}); assert_eq!( service.extract_gts_id(&entity), - Some("gts.acme.core.events.test.v1~".to_owned()) + Some(gts_id!("acme.core.events.test.v1~").to_owned()) ); - let entity = json!({"gtsId": "gts.acme.core.events.test.v1~"}); + let entity = json!({"gtsId": gts_id!("acme.core.events.test.v1~")}); assert_eq!( service.extract_gts_id(&entity), - Some("gts.acme.core.events.test.v1~".to_owned()) + Some(gts_id!("acme.core.events.test.v1~").to_owned()) ); let entity = json!({"other": "value"}); @@ -308,8 +310,8 @@ mod tests { ); let entities = vec![ - json!({"$id": "gts://gts.acme.core.events.test.v1~"}), - json!({"$id": "gts://gts.acme.core.events.test2.v1~"}), + json!({"$id": gts_uri!("acme.core.events.test.v1~")}), + json!({"$id": gts_uri!("acme.core.events.test2.v1~")}), ]; let results = service.register(entities); @@ -326,8 +328,8 @@ mod tests { ); let entities = vec![ - json!({"$id": "gts://gts.acme.core.events.test.v1~"}), - json!({"$id": "gts://gts.acme.core.events.fail.v1~"}), + json!({"$id": gts_uri!("acme.core.events.test.v1~")}), + json!({"$id": gts_uri!("acme.core.events.fail.v1~")}), json!({"other": "no id"}), ]; @@ -344,7 +346,7 @@ mod tests { Arc::new(MockRepo::new()), crate::config::TypesRegistryConfig::default(), ); - let result = service.get("gts.acme.core.events.test.v1~"); + let result = service.get(gts_id!("acme.core.events.test.v1~")); assert!(result.is_ok()); } @@ -354,7 +356,7 @@ mod tests { Arc::new(MockRepo::new()), crate::config::TypesRegistryConfig::default(), ); - let result = service.get("gts.vendor.pkg.ns.notfound.v1~"); + let result = service.get(gts_id!("vendor.pkg.ns.notfound.v1~")); assert!(result.is_err()); } @@ -393,7 +395,7 @@ mod tests { match result.unwrap_err() { DomainError::ReadyCommitFailed(errors) => { assert_eq!(errors.len(), 2); - assert_eq!(errors[0].gts_id, "gts.test1~"); + assert_eq!(errors[0].gts_id, format!("{GTS_ID_PREFIX}test1~")); assert_eq!(errors[0].message, "error1"); } _ => panic!("Expected ReadyCommitFailed"), diff --git a/gears/system/types-registry/types-registry/src/infra/cache/cache_tests.rs b/gears/system/types-registry/types-registry/src/infra/cache/cache_tests.rs index 43948211c..9e30a6bb9 100644 --- a/gears/system/types-registry/types-registry/src/infra/cache/cache_tests.rs +++ b/gears/system/types-registry/types-registry/src/infra/cache/cache_tests.rs @@ -7,6 +7,8 @@ use super::*; use serde_json::json; use std::thread::sleep; +use toolkit_gts::GTS_ID_PREFIX; +use toolkit_gts::gts_id; use types_registry_sdk::GtsTypeId; fn make_type_schema(type_id: &str) -> Arc { @@ -17,16 +19,16 @@ fn make_type_schema(type_id: &str) -> Arc { fn test_get_miss_returns_none() { let cache: InMemoryCache> = InMemoryCache::>::new(CacheConfig::type_schemas()); - assert!(cache.get("gts.cf.y.z.t.v1~").is_none()); + assert!(cache.get(gts_id!("cf.y.z.t.v1~")).is_none()); } #[test] fn test_put_then_get_returns_value() { let cache: InMemoryCache> = InMemoryCache::>::new(CacheConfig::type_schemas()); - let schema = make_type_schema("gts.cf.y.z.t.v1~"); - cache.put("gts.cf.y.z.t.v1~".to_owned(), Arc::clone(&schema)); - let hit = cache.get("gts.cf.y.z.t.v1~").unwrap(); + let schema = make_type_schema(gts_id!("cf.y.z.t.v1~")); + cache.put(gts_id!("cf.y.z.t.v1~").to_owned(), Arc::clone(&schema)); + let hit = cache.get(gts_id!("cf.y.z.t.v1~")).unwrap(); assert!(Arc::ptr_eq(&hit, &schema)); } @@ -34,18 +36,18 @@ fn test_put_then_get_returns_value() { fn test_invalidate_removes_entry() { let cache: InMemoryCache> = InMemoryCache::>::new(CacheConfig::type_schemas()); - let schema = make_type_schema("gts.cf.y.z.t.v1~"); - cache.put("gts.cf.y.z.t.v1~".to_owned(), schema); - cache.invalidate("gts.cf.y.z.t.v1~"); - assert!(cache.get("gts.cf.y.z.t.v1~").is_none()); + let schema = make_type_schema(gts_id!("cf.y.z.t.v1~")); + cache.put(gts_id!("cf.y.z.t.v1~").to_owned(), schema); + cache.invalidate(gts_id!("cf.y.z.t.v1~")); + assert!(cache.get(gts_id!("cf.y.z.t.v1~")).is_none()); } #[test] fn test_clear_drops_all() { let cache: InMemoryCache> = InMemoryCache::>::new(CacheConfig::type_schemas()); - cache.put("a~".to_owned(), make_type_schema("gts.cf.y.z.a.v1~")); - cache.put("b~".to_owned(), make_type_schema("gts.cf.y.z.b.v1~")); + cache.put("a~".to_owned(), make_type_schema(gts_id!("cf.y.z.a.v1~"))); + cache.put("b~".to_owned(), make_type_schema(gts_id!("cf.y.z.b.v1~"))); assert_eq!(cache.len(), 2); cache.clear(); assert!(cache.is_empty()); @@ -56,9 +58,9 @@ fn test_lru_eviction_at_capacity() { let cache: InMemoryCache> = InMemoryCache::>::new( CacheConfig::type_schemas().with_capacity(2).without_ttl(), ); - cache.put("a".to_owned(), make_type_schema("gts.cf.y.z.a.v1~")); - cache.put("b".to_owned(), make_type_schema("gts.cf.y.z.b.v1~")); - cache.put("c".to_owned(), make_type_schema("gts.cf.y.z.c.v1~")); + cache.put("a".to_owned(), make_type_schema(gts_id!("cf.y.z.a.v1~"))); + cache.put("b".to_owned(), make_type_schema(gts_id!("cf.y.z.b.v1~"))); + cache.put("c".to_owned(), make_type_schema(gts_id!("cf.y.z.c.v1~"))); // a was the least recently used → evicted. assert!(cache.get("a").is_none()); assert!(cache.get("b").is_some()); @@ -70,7 +72,7 @@ fn test_ttl_expiry() { let cache: InMemoryCache> = InMemoryCache::>::new( CacheConfig::type_schemas().with_ttl(Duration::from_millis(50)), ); - cache.put("a".to_owned(), make_type_schema("gts.cf.y.z.a.v1~")); + cache.put("a".to_owned(), make_type_schema(gts_id!("cf.y.z.a.v1~"))); assert!(cache.get("a").is_some()); sleep(Duration::from_millis(80)); assert!(cache.get("a").is_none()); @@ -81,7 +83,7 @@ fn test_no_ttl_keeps_entry() { let cache: InMemoryCache> = InMemoryCache::>::new( CacheConfig::type_schemas().without_ttl().with_capacity(8), ); - cache.put("a".to_owned(), make_type_schema("gts.cf.y.z.a.v1~")); + cache.put("a".to_owned(), make_type_schema(gts_id!("cf.y.z.a.v1~"))); sleep(Duration::from_millis(20)); assert!(cache.get("a").is_some()); } @@ -93,9 +95,9 @@ fn test_zero_capacity_clamped_to_one() { capacity: 0, ttl: None, }); - cache.put("a".to_owned(), make_type_schema("gts.cf.y.z.a.v1~")); + cache.put("a".to_owned(), make_type_schema(gts_id!("cf.y.z.a.v1~"))); // capacity = 1 means second put evicts the first. - cache.put("b".to_owned(), make_type_schema("gts.cf.y.z.b.v1~")); + cache.put("b".to_owned(), make_type_schema(gts_id!("cf.y.z.b.v1~"))); assert!(cache.get("a").is_none()); assert!(cache.get("b").is_some()); } @@ -117,15 +119,15 @@ fn test_default_configs() { fn test_get_many_preserves_order_and_gaps() { let cache: InMemoryCache> = InMemoryCache::>::new(CacheConfig::type_schemas()); - let a = make_type_schema("gts.cf.y.z.a.v1~"); - let c = make_type_schema("gts.cf.y.z.c.v1~"); - cache.put("gts.cf.y.z.a.v1~".to_owned(), Arc::clone(&a)); - cache.put("gts.cf.y.z.c.v1~".to_owned(), Arc::clone(&c)); + let a = make_type_schema(gts_id!("cf.y.z.a.v1~")); + let c = make_type_schema(gts_id!("cf.y.z.c.v1~")); + cache.put(gts_id!("cf.y.z.a.v1~").to_owned(), Arc::clone(&a)); + cache.put(gts_id!("cf.y.z.c.v1~").to_owned(), Arc::clone(&c)); let results = cache.get_many(&[ - "gts.cf.y.z.a.v1~", - "gts.cf.y.z.b.v1~", // not in cache - "gts.cf.y.z.c.v1~", + gts_id!("cf.y.z.a.v1~"), + gts_id!("cf.y.z.b.v1~"), // not in cache + gts_id!("cf.y.z.c.v1~"), ]); assert_eq!(results.len(), 3); assert!(Arc::ptr_eq(results[0].as_ref().unwrap(), &a)); @@ -145,7 +147,7 @@ fn test_get_many_empty_input() { fn test_get_many_all_misses() { let cache: InMemoryCache> = InMemoryCache::>::new(CacheConfig::type_schemas()); - let results = cache.get_many(&["gts.cf.y.z.a.v1~", "gts.cf.y.z.b.v1~"]); + let results = cache.get_many(&[gts_id!("cf.y.z.a.v1~"), gts_id!("cf.y.z.b.v1~")]); assert_eq!(results.len(), 2); assert!(results[0].is_none()); assert!(results[1].is_none()); @@ -160,23 +162,23 @@ fn test_get_many_evicts_expired_entries() { CacheConfig::type_schemas().with_ttl(Duration::from_millis(50)), ); cache.put( - "gts.cf.y.z.expired.v1~".to_owned(), - make_type_schema("gts.cf.y.z.expired.v1~"), + gts_id!("cf.y.z.expired.v1~").to_owned(), + make_type_schema(gts_id!("cf.y.z.expired.v1~")), ); sleep(Duration::from_millis(80)); // Fresh entry inserted AFTER the sleep — its TTL clock is reset. cache.put( - "gts.cf.y.z.fresh.v1~".to_owned(), - make_type_schema("gts.cf.y.z.fresh.v1~"), + gts_id!("cf.y.z.fresh.v1~").to_owned(), + make_type_schema(gts_id!("cf.y.z.fresh.v1~")), ); - let results = cache.get_many(&["gts.cf.y.z.expired.v1~", "gts.cf.y.z.fresh.v1~"]); + let results = cache.get_many(&[gts_id!("cf.y.z.expired.v1~"), gts_id!("cf.y.z.fresh.v1~")]); assert!(results[0].is_none()); assert!(results[1].is_some()); // Expired entry must be evicted as a side effect. - assert!(cache.get("gts.cf.y.z.expired.v1~").is_none()); - assert!(cache.get("gts.cf.y.z.fresh.v1~").is_some()); + assert!(cache.get(gts_id!("cf.y.z.expired.v1~")).is_none()); + assert!(cache.get(gts_id!("cf.y.z.fresh.v1~")).is_some()); assert_eq!(cache.len(), 1); } @@ -188,35 +190,38 @@ fn test_put_many_inserts_all_entries() { InMemoryCache::>::new(CacheConfig::type_schemas()); cache.put_many(vec![ ( - "gts.cf.y.z.a.v1~".to_owned(), - make_type_schema("gts.cf.y.z.a.v1~"), + gts_id!("cf.y.z.a.v1~").to_owned(), + make_type_schema(gts_id!("cf.y.z.a.v1~")), ), ( - "gts.cf.y.z.b.v1~".to_owned(), - make_type_schema("gts.cf.y.z.b.v1~"), + gts_id!("cf.y.z.b.v1~").to_owned(), + make_type_schema(gts_id!("cf.y.z.b.v1~")), ), ( - "gts.cf.y.z.c.v1~".to_owned(), - make_type_schema("gts.cf.y.z.c.v1~"), + gts_id!("cf.y.z.c.v1~").to_owned(), + make_type_schema(gts_id!("cf.y.z.c.v1~")), ), ]); assert_eq!(cache.len(), 3); - assert!(cache.get("gts.cf.y.z.a.v1~").is_some()); - assert!(cache.get("gts.cf.y.z.b.v1~").is_some()); - assert!(cache.get("gts.cf.y.z.c.v1~").is_some()); + assert!(cache.get(gts_id!("cf.y.z.a.v1~")).is_some()); + assert!(cache.get(gts_id!("cf.y.z.b.v1~")).is_some()); + assert!(cache.get(gts_id!("cf.y.z.c.v1~")).is_some()); } #[test] fn test_put_many_replaces_existing() { let cache: InMemoryCache> = InMemoryCache::>::new(CacheConfig::type_schemas()); - let v1 = make_type_schema("gts.cf.y.z.a.v1~"); - let v2 = make_type_schema("gts.cf.y.z.a.v1~"); - cache.put("gts.cf.y.z.a.v1~".to_owned(), Arc::clone(&v1)); - cache.put_many(vec![("gts.cf.y.z.a.v1~".to_owned(), Arc::clone(&v2))]); + let v1 = make_type_schema(gts_id!("cf.y.z.a.v1~")); + let v2 = make_type_schema(gts_id!("cf.y.z.a.v1~")); + cache.put(gts_id!("cf.y.z.a.v1~").to_owned(), Arc::clone(&v1)); + cache.put_many(vec![(gts_id!("cf.y.z.a.v1~").to_owned(), Arc::clone(&v2))]); assert_eq!(cache.len(), 1); // The replacement won — same key now points to v2's Arc. - assert!(Arc::ptr_eq(&cache.get("gts.cf.y.z.a.v1~").unwrap(), &v2)); + assert!(Arc::ptr_eq( + &cache.get(gts_id!("cf.y.z.a.v1~")).unwrap(), + &v2 + )); } #[test] @@ -235,22 +240,22 @@ fn test_put_many_respects_capacity() { ); cache.put_many(vec![ ( - "gts.cf.y.z.a.v1~".to_owned(), - make_type_schema("gts.cf.y.z.a.v1~"), + gts_id!("cf.y.z.a.v1~").to_owned(), + make_type_schema(gts_id!("cf.y.z.a.v1~")), ), ( - "gts.cf.y.z.b.v1~".to_owned(), - make_type_schema("gts.cf.y.z.b.v1~"), + gts_id!("cf.y.z.b.v1~").to_owned(), + make_type_schema(gts_id!("cf.y.z.b.v1~")), ), ( - "gts.cf.y.z.c.v1~".to_owned(), - make_type_schema("gts.cf.y.z.c.v1~"), + gts_id!("cf.y.z.c.v1~").to_owned(), + make_type_schema(gts_id!("cf.y.z.c.v1~")), ), ]); assert_eq!(cache.len(), 2); - assert!(cache.get("gts.cf.y.z.a.v1~").is_none()); // evicted (LRU) - assert!(cache.get("gts.cf.y.z.b.v1~").is_some()); - assert!(cache.get("gts.cf.y.z.c.v1~").is_some()); + assert!(cache.get(gts_id!("cf.y.z.a.v1~")).is_none()); // evicted (LRU) + assert!(cache.get(gts_id!("cf.y.z.b.v1~")).is_some()); + assert!(cache.get(gts_id!("cf.y.z.c.v1~")).is_some()); } // ── reverse `uuid → gts_id` index pruning ──────────────────────────────── @@ -265,7 +270,7 @@ fn test_reverse_index_pruned_on_capacity_eviction() { CacheConfig::type_schemas().with_capacity(2).without_ttl(), ); let schemas: Vec> = (0..5) - .map(|i| make_type_schema(&format!("gts.cf.y.z.s{i}.v1~"))) + .map(|i| make_type_schema(&format!("{GTS_ID_PREFIX}cf.y.z.s{i}.v1~"))) .collect(); for s in &schemas { cache.put(s.type_id.to_string(), Arc::clone(s)); @@ -287,11 +292,11 @@ fn test_reverse_index_pruned_on_capacity_eviction() { fn test_reverse_index_pruned_on_invalidate() { let cache: InMemoryCache> = InMemoryCache::>::new(CacheConfig::type_schemas()); - let s = make_type_schema("gts.cf.y.z.t.v1~"); + let s = make_type_schema(gts_id!("cf.y.z.t.v1~")); let uuid = s.type_uuid; cache.put(s.type_id.to_string(), s); assert!(cache.get_by_uuid(uuid).is_some()); - cache.invalidate("gts.cf.y.z.t.v1~"); + cache.invalidate(gts_id!("cf.y.z.t.v1~")); // After invalidate, the reverse index must not retain a dangling // mapping that points at a now-evicted LRU entry. assert!(cache.get_by_uuid(uuid).is_none()); @@ -302,9 +307,9 @@ fn test_reverse_index_pruned_on_retain() { let cache: InMemoryCache> = InMemoryCache::>::new( CacheConfig::type_schemas().with_capacity(4).without_ttl(), ); - let kept = make_type_schema("gts.cf.y.z.keep.v1~"); - let first_dropped = make_type_schema("gts.cf.y.z.drop_a.v1~"); - let second_dropped = make_type_schema("gts.cf.y.z.drop_b.v1~"); + let kept = make_type_schema(gts_id!("cf.y.z.keep.v1~")); + let first_dropped = make_type_schema(gts_id!("cf.y.z.drop_a.v1~")); + let second_dropped = make_type_schema(gts_id!("cf.y.z.drop_b.v1~")); let kept_uuid = kept.type_uuid; let first_uuid = first_dropped.type_uuid; let second_uuid = second_dropped.type_uuid; @@ -325,13 +330,13 @@ fn test_reverse_index_pruned_on_ttl_expiry() { let cache: InMemoryCache> = InMemoryCache::>::new( CacheConfig::type_schemas().with_ttl(Duration::from_millis(50)), ); - let s = make_type_schema("gts.cf.y.z.t.v1~"); + let s = make_type_schema(gts_id!("cf.y.z.t.v1~")); let uuid = s.type_uuid; cache.put(s.type_id.to_string(), s); sleep(Duration::from_millis(80)); // First lookup observes expiry and evicts the LRU entry; the // reverse mapping must drop in the same step so the second lookup // doesn't see a dangling pointer. - assert!(cache.get("gts.cf.y.z.t.v1~").is_none()); + assert!(cache.get(gts_id!("cf.y.z.t.v1~")).is_none()); assert!(cache.get_by_uuid(uuid).is_none()); } diff --git a/gears/system/types-registry/types-registry/src/infra/storage/debug_diagnostics.rs b/gears/system/types-registry/types-registry/src/infra/storage/debug_diagnostics.rs index 2809f4411..99110767f 100644 --- a/gears/system/types-registry/types-registry/src/infra/storage/debug_diagnostics.rs +++ b/gears/system/types-registry/types-registry/src/infra/storage/debug_diagnostics.rs @@ -7,6 +7,7 @@ //! - Cycle detection for circular schema references use std::collections::HashSet; +use toolkit_gts::GTS_ID_URI_PREFIX; use gts::GtsOps; use serde_json::Value; @@ -217,13 +218,16 @@ fn collect_schema_refs(schema: &Value) -> Vec { /// Normalizes a reference to a GTS ID. /// /// Handles both: -/// - Direct GTS IDs: `gts.vendor.pkg.ns.type.v1~` -/// - URI format: `gts://gts.vendor.pkg.ns.type.v1~` +/// - Direct GTS IDs +/// - GTS URI format fn normalize_gts_ref(ref_val: &str) -> Option { - let cleaned = ref_val.strip_prefix("gts://").unwrap_or(ref_val); + let cleaned = ref_val.strip_prefix(GTS_ID_URI_PREFIX).unwrap_or(ref_val); - // Only return if it looks like a GTS ID - if cleaned.starts_with("gts.") { + // Only return if it parses as a concrete GTS ID under the configured + // prefix. A bare prefix check would miss malformed-but-prefixed ids and + // would silently hard-code the prefix; the parser is the spec source of + // truth. + if gts::GtsId::try_new(cleaned).is_ok() { Some(cleaned.to_owned()) } else { None @@ -234,47 +238,49 @@ fn normalize_gts_ref(ref_val: &str) -> Option { mod tests { use super::*; use serde_json::json; + use toolkit_gts::{gts_id, gts_uri}; #[test] fn test_extract_schema_id() { - assert_eq!( - extract_schema_id("gts.vendor.pkg.ns.type.v1~vendor.app.instance.v1"), - Some("gts.vendor.pkg.ns.type.v1~".to_owned()) - ); + const INSTANCE_ID: &str = gts_id!("vendor.pkg.ns.type.v1~vendor.app.ns.instance.v1"); + const SCHEMA_ID: &str = gts_id!("vendor.pkg.ns.type.v1~"); + + assert_eq!(extract_schema_id(INSTANCE_ID), Some(SCHEMA_ID.to_owned())); // Schema IDs (ending with ~) don't have an "instance" portion - assert_eq!(extract_schema_id("gts.vendor.pkg.ns.type.v1~"), None); + assert_eq!(extract_schema_id(SCHEMA_ID), None); assert_eq!(extract_schema_id("no-tilde"), None); } #[test] fn test_normalize_gts_ref() { - assert_eq!( - normalize_gts_ref("gts://gts.vendor.pkg.ns.type.v1~"), - Some("gts.vendor.pkg.ns.type.v1~".to_owned()) - ); - assert_eq!( - normalize_gts_ref("gts.vendor.pkg.ns.type.v1~"), - Some("gts.vendor.pkg.ns.type.v1~".to_owned()) - ); + const SCHEMA_ID: &str = gts_id!("vendor.pkg.ns.type.v1~"); + const SCHEMA_URI: &str = gts_uri!("vendor.pkg.ns.type.v1~"); + + assert_eq!(normalize_gts_ref(SCHEMA_URI), Some(SCHEMA_ID.to_owned())); + assert_eq!(normalize_gts_ref(SCHEMA_ID), Some(SCHEMA_ID.to_owned())); assert_eq!(normalize_gts_ref("#/definitions/Something"), None); assert_eq!(normalize_gts_ref("http://example.com/schema"), None); } #[test] fn test_collect_schema_refs() { + const BASE_ID: &str = gts_id!("vendor.pkg.ns.base.v1~"); + const MIXIN_ID: &str = gts_id!("vendor.pkg.ns.mixin.v1~"); + const OTHER_ID: &str = gts_id!("vendor.pkg.ns.other.v1~"); + let schema = json!({ - "$ref": "gts://gts.vendor.pkg.ns.base.v1~", + "$ref": gts_uri!("vendor.pkg.ns.base.v1~"), "allOf": [ - { "$ref": "gts.vendor.pkg.ns.mixin.v1~" } + { "$ref": MIXIN_ID } ], - "x-gts-ref": "gts.vendor.pkg.ns.other.v1~" + "x-gts-ref": OTHER_ID }); let refs = collect_schema_refs(&schema); assert_eq!(refs.len(), 3); - assert!(refs.contains(&"gts.vendor.pkg.ns.base.v1~".to_owned())); - assert!(refs.contains(&"gts.vendor.pkg.ns.mixin.v1~".to_owned())); - assert!(refs.contains(&"gts.vendor.pkg.ns.other.v1~".to_owned())); + assert!(refs.contains(&BASE_ID.to_owned())); + assert!(refs.contains(&MIXIN_ID.to_owned())); + assert!(refs.contains(&OTHER_ID.to_owned())); } #[test] @@ -293,7 +299,7 @@ mod tests { #[test] fn test_cycle_detection_in_visited_set() { let mut visited: HashSet = HashSet::new(); - let type_id = "gts.vendor.pkg.ns.type.v1~"; + let type_id = gts_id!("vendor.pkg.ns.type.v1~"); // First visit should succeed assert!(!visited.contains(type_id)); @@ -306,11 +312,12 @@ mod tests { #[test] fn test_log_registration_failure_with_gts_id() { // This test verifies the function doesn't panic + const TYPE_ID: &str = gts_id!("acme.core.events.test.v1~"); let entity = json!({ - "$id": "gts://gts.acme.core.events.test.v1~", + "$id": gts_uri!("acme.core.events.test.v1~"), "type": "object" }); - log_registration_failure(Some("gts.acme.core.events.test.v1~"), &entity, "Test error"); + log_registration_failure(Some(TYPE_ID), &entity, "Test error"); } #[test] @@ -325,36 +332,39 @@ mod tests { #[test] fn test_log_schema_validation_failure() { // This test verifies the function doesn't panic + const TYPE_ID: &str = gts_id!("acme.core.events.test.v1~"); let schema = json!({ - "$id": "gts://gts.acme.core.events.test.v1~", + "$id": gts_uri!("acme.core.events.test.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "invalid_type" }); - log_schema_validation_failure("gts.acme.core.events.test.v1~", &schema, "Invalid type"); + log_schema_validation_failure(TYPE_ID, &schema, "Invalid type"); } #[test] fn test_extract_schema_id_with_chained_instance() { // Instance with multiple segments - assert_eq!( - extract_schema_id("gts.a.b.c.d.v1~vendor.app.x.y.v1"), - Some("gts.a.b.c.d.v1~".to_owned()) - ); + const INSTANCE_ID: &str = gts_id!("a.b.c.d.v1~vendor.app.x.y.v1"); + const SCHEMA_ID: &str = gts_id!("a.b.c.d.v1~"); + assert_eq!(extract_schema_id(INSTANCE_ID), Some(SCHEMA_ID.to_owned())); } #[test] fn test_collect_schema_refs_nested_allof() { + const BASE1_ID: &str = gts_id!("vendor.pkg.ns.base1.v1~"); + const BASE2_ID: &str = gts_id!("vendor.pkg.ns.base2.v1~"); + let schema = json!({ "allOf": [ - { "$ref": "gts.vendor.pkg.ns.base1.v1~" }, - { "$ref": "gts.vendor.pkg.ns.base2.v1~" }, + { "$ref": BASE1_ID }, + { "$ref": BASE2_ID }, { "type": "object" } ] }); let refs = collect_schema_refs(&schema); assert_eq!(refs.len(), 2); - assert!(refs.contains(&"gts.vendor.pkg.ns.base1.v1~".to_owned())); - assert!(refs.contains(&"gts.vendor.pkg.ns.base2.v1~".to_owned())); + assert!(refs.contains(&BASE1_ID.to_owned())); + assert!(refs.contains(&BASE2_ID.to_owned())); } } diff --git a/gears/system/types-registry/types-registry/src/infra/storage/in_memory_repo.rs b/gears/system/types-registry/types-registry/src/infra/storage/in_memory_repo.rs index 0076ee1f3..da34897bc 100644 --- a/gears/system/types-registry/types-registry/src/infra/storage/in_memory_repo.rs +++ b/gears/system/types-registry/types-registry/src/infra/storage/in_memory_repo.rs @@ -1,8 +1,9 @@ //! In-memory repository implementation using gts-rust. use std::sync::atomic::{AtomicBool, Ordering}; +use toolkit_gts::GTS_ID_URI_PREFIX; -use gts::{GtsConfig, GtsID, GtsIdSegment, GtsOps, GtsWildcard}; +use gts::{GtsConfig, GtsId, GtsIdPattern, GtsIdSegment, GtsOps}; use parking_lot::Mutex; use uuid::Uuid; @@ -46,9 +47,10 @@ impl InMemoryGtsRepository { /// Converts a gts-rust entity result to our SDK `GtsEntity`. fn to_gts_entity(gts_id: &str, content: &serde_json::Value) -> Result { - let parsed = GtsID::new(gts_id).map_err(|e| DomainError::invalid_gts_id(e.to_string()))?; + let parsed = + GtsId::try_new(gts_id).map_err(|e| DomainError::invalid_gts_id(e.to_string()))?; - let segments: Vec = parsed.gts_id_segments.clone(); + let segments: Vec = parsed.segments().to_vec(); let is_schema = gts_id.ends_with('~'); @@ -71,14 +73,14 @@ impl InMemoryGtsRepository { /// Extracts the GTS ID from an entity JSON value using configured fields. /// - /// Strips the `gts://` URI prefix from `$id` fields for JSON Schema compatibility (gts-rust v0.7.0+). + /// Strips the GTS URI prefix from `$id` fields for JSON Schema compatibility. fn extract_gts_id(&self, entity: &serde_json::Value) -> Option { if let Some(obj) = entity.as_object() { for field in &self.config.entity_id_fields { if let Some(id) = obj.get(field).and_then(|v| v.as_str()) { - // Strip gts:// prefix from $id field (JSON Schema URI format) + // Strip GTS URI prefix from $id field (JSON Schema URI format) let cleaned_id = if field == "$id" { - id.strip_prefix("gts://").unwrap_or(id) + id.strip_prefix(GTS_ID_URI_PREFIX).unwrap_or(id) } else { id }; @@ -93,17 +95,17 @@ impl InMemoryGtsRepository { /// /// The pattern is parsed once at the start of [`Self::list`] (so an /// invalid pattern fails the whole call rather than silently passing - /// through every entity) and the resulting [`GtsWildcard`] is threaded + /// through every entity) and the resulting [`GtsIdPattern`] is threaded /// in here. fn matches_query( entity: &GtsEntity, - wildcard: Option<&GtsWildcard>, + wildcard: Option<&GtsIdPattern>, query: &ListQuery, ) -> bool { if let Some(wildcard) = wildcard { - match GtsID::new(&entity.gts_id) { + match GtsId::try_new(&entity.gts_id) { Ok(gts_id) => { - if !gts_id.wildcard_match(wildcard) { + if !gts_id.matches_pattern(wildcard) { return false; } } @@ -126,19 +128,21 @@ impl InMemoryGtsRepository { }; if let Some(ref vendor) = query.vendor - && !segments_to_check.iter().any(|s| s.vendor == *vendor) + && !segments_to_check.iter().any(|s| s.vendor() == *vendor) { return false; } if let Some(ref package) = query.package - && !segments_to_check.iter().any(|s| s.package == *package) + && !segments_to_check.iter().any(|s| s.package() == *package) { return false; } if let Some(ref namespace) = query.namespace - && !segments_to_check.iter().any(|s| s.namespace == *namespace) + && !segments_to_check + .iter() + .any(|s| s.namespace() == *namespace) { return false; } @@ -157,7 +161,7 @@ impl GtsRepository for InMemoryGtsRepository { .extract_gts_id(entity) .ok_or_else(|| DomainError::invalid_gts_id("No GTS ID field found in entity"))?; - GtsID::new(>s_id).map_err(|e| DomainError::invalid_gts_id(e.to_string()))?; + GtsId::try_new(>s_id).map_err(|e| DomainError::invalid_gts_id(e.to_string()))?; if self.is_ready.load(Ordering::SeqCst) { let mut persistent = self.persistent.lock(); @@ -225,7 +229,7 @@ impl GtsRepository for InMemoryGtsRepository { let persistent = self.persistent.lock(); for (gts_id, gts_entity) in persistent.store.items() { // UUIDs are deterministic v5 from gts_id; recompute and compare. - if let Ok(parsed) = GtsID::new(gts_id) + if let Ok(parsed) = GtsId::try_new(gts_id) && parsed.to_uuid() == id { return Self::to_gts_entity(gts_id, >s_entity.content); @@ -237,7 +241,7 @@ impl GtsRepository for InMemoryGtsRepository { fn list(&self, query: &ListQuery) -> Result, DomainError> { // Validate the pattern once up front. `None` means "no filter". // `Some("")` is a caller bug — it's distinct from `None` but can't - // mean anything meaningful, and `GtsWildcard::new("")` may not + // mean anything meaningful, and `GtsIdPattern::try_new("")` may not // surface a user-friendly diagnostic. An invalid wildcard (multiple // `*`s, mid-pattern `*`, segment-boundary violation — see GTS spec // section 10) is also a caller bug that previously slipped through @@ -249,7 +253,7 @@ impl GtsRepository for InMemoryGtsRepository { "pattern is empty (use `None` to mean \"no filter\")", )); } - Some(p) => Some(GtsWildcard::new(p).map_err(|e| { + Some(p) => Some(GtsIdPattern::try_new(p).map_err(|e| { DomainError::invalid_query(format!("invalid GTS wildcard pattern `{p}`: {e}")) })?), None => None, @@ -366,8 +370,16 @@ impl GtsRepository for InMemoryGtsRepository { mod tests { use super::*; use serde_json::json; + use toolkit_gts::{GTS_ID_PREFIX, gts_id, gts_uri}; const JSON_SCHEMA_DRAFT_07: &str = "http://json-schema.org/draft-07/schema#"; + const USER_CREATED_ID: &str = gts_id!("acme.core.events.user_created.v1~"); + const ORDER_PLACED_ID: &str = gts_id!("globex.core.events.order_placed.v1~"); + const MISSING_ID: &str = gts_id!("fabrikam.pkg.ns.type.v1~"); + + fn schema_uri(id: &str) -> String { + gts_uri!(id) + } fn default_config() -> GtsConfig { crate::config::TypesRegistryConfig::default().to_gts_config() @@ -378,7 +390,7 @@ mod tests { let repo = InMemoryGtsRepository::new(default_config()); let entity = json!({ - "$id": "gts://gts.acme.core.events.user_created.v1~", + "$id": schema_uri(USER_CREATED_ID), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object", "properties": { @@ -390,7 +402,7 @@ mod tests { assert!(result.is_ok()); let registered = result.unwrap(); - assert_eq!(registered.gts_id, "gts.acme.core.events.user_created.v1~"); + assert_eq!(registered.gts_id, USER_CREATED_ID); assert!(registered.is_type()); } @@ -399,7 +411,7 @@ mod tests { let repo = InMemoryGtsRepository::new(default_config()); let entity = json!({ - "$id": "gts://gts.acme.core.events.user_created.v1~", + "$id": schema_uri(USER_CREATED_ID), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object" }); @@ -416,13 +428,13 @@ mod tests { let repo = InMemoryGtsRepository::new(default_config()); let entity1 = json!({ - "$id": "gts://gts.acme.core.events.user_created.v1~", + "$id": schema_uri(USER_CREATED_ID), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object" }); let entity2 = json!({ - "$id": "gts://gts.acme.core.events.user_created.v1~", + "$id": schema_uri(USER_CREATED_ID), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object", "description": "Different content" @@ -467,7 +479,7 @@ mod tests { let repo = InMemoryGtsRepository::new(default_config()); let entity = json!({ - "$id": "gts://gts.acme.core.events.user_created.v1~", + "$id": schema_uri(USER_CREATED_ID), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object", "properties": { @@ -483,7 +495,7 @@ mod tests { assert!(result.is_ok()); assert!(repo.is_ready()); - let get_result = repo.get("gts.acme.core.events.user_created.v1~"); + let get_result = repo.get(USER_CREATED_ID); assert!(get_result.is_ok()); } @@ -492,12 +504,12 @@ mod tests { let repo = InMemoryGtsRepository::new(default_config()); let type1 = json!({ - "$id": "gts://gts.acme.core.events.user_created.v1~", + "$id": schema_uri(USER_CREATED_ID), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object" }); let type2 = json!({ - "$id": "gts://gts.globex.core.events.order_placed.v1~", + "$id": schema_uri(ORDER_PLACED_ID), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object" }); @@ -515,7 +527,7 @@ mod tests { let repo = InMemoryGtsRepository::new(default_config()); repo.switch_to_ready().unwrap(); - let result = repo.get("gts.fabrikam.pkg.ns.type.v1~"); + let result = repo.get(MISSING_ID); assert!(matches!(result, Err(DomainError::NotFound { .. }))); } @@ -525,7 +537,7 @@ mod tests { repo.switch_to_ready().unwrap(); let entity = json!({ - "$id": "gts://gts.acme.core.events.user_created.v1~", + "$id": schema_uri(USER_CREATED_ID), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object" }); @@ -533,7 +545,7 @@ mod tests { let result = repo.register(&entity, true); assert!(result.is_ok()); - let get_result = repo.get("gts.acme.core.events.user_created.v1~"); + let get_result = repo.get(USER_CREATED_ID); assert!(get_result.is_ok()); } @@ -543,7 +555,7 @@ mod tests { repo.switch_to_ready().unwrap(); let entity = json!({ - "$id": "gts://gts.acme.core.events.user_created.v1~", + "$id": schema_uri(USER_CREATED_ID), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object" }); @@ -562,13 +574,13 @@ mod tests { repo.switch_to_ready().unwrap(); let entity1 = json!({ - "$id": "gts://gts.acme.core.events.user_created.v1~", + "$id": schema_uri(USER_CREATED_ID), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object" }); let entity2 = json!({ - "$id": "gts://gts.acme.core.events.user_created.v1~", + "$id": schema_uri(USER_CREATED_ID), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object", "description": "Different content" @@ -584,7 +596,7 @@ mod tests { let repo = InMemoryGtsRepository::new(default_config()); let entity = json!({ - "$id": "gts://gts.acme.core.events.user_created.v1~", + "$id": schema_uri(USER_CREATED_ID), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object" }); @@ -592,8 +604,8 @@ mod tests { repo.register(&entity, false).unwrap(); repo.switch_to_ready().unwrap(); - assert!(repo.exists("gts.acme.core.events.user_created.v1~")); - assert!(!repo.exists("gts.fabrikam.pkg.ns.type.v1~")); + assert!(repo.exists(USER_CREATED_ID)); + assert!(!repo.exists(MISSING_ID)); } #[test] @@ -601,7 +613,7 @@ mod tests { let repo = InMemoryGtsRepository::new(default_config()); let type_entity = json!({ - "$id": "gts://gts.acme.core.events.user_created.v1~", + "$id": schema_uri(USER_CREATED_ID), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object" }); @@ -623,7 +635,7 @@ mod tests { let repo = InMemoryGtsRepository::new(default_config()); let entity = json!({ - "$id": "gts://gts.acme.core.events.user_created.v1~", + "$id": schema_uri(USER_CREATED_ID), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object" }); @@ -631,11 +643,11 @@ mod tests { repo.register(&entity, false).unwrap(); repo.switch_to_ready().unwrap(); - let query = ListQuery::default().with_pattern("gts.acme.*"); + let query = ListQuery::default().with_pattern(format!("{GTS_ID_PREFIX}acme.*")); let results = repo.list(&query).unwrap(); assert_eq!(results.len(), 1); - let query = ListQuery::default().with_pattern("gts.contoso.*"); + let query = ListQuery::default().with_pattern(format!("{GTS_ID_PREFIX}contoso.*")); let results = repo.list(&query).unwrap(); assert_eq!(results.len(), 0); } @@ -672,7 +684,7 @@ mod tests { let repo = InMemoryGtsRepository::new(default_config()); repo.register( &json!({ - "$id": "gts://gts.acme.core.events.x.v1~", + "$id": schema_uri(gts_id!("acme.core.events.x.v1~")), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object" }), @@ -682,11 +694,12 @@ mod tests { repo.switch_to_ready().unwrap(); // Multi-wildcard pattern → invalid per spec. - let query = ListQuery::default().with_pattern("gts.*.*.rg.*"); + let invalid_pattern = format!("{GTS_ID_PREFIX}*.*.rg.*"); + let query = ListQuery::default().with_pattern(invalid_pattern.clone()); match repo.list(&query) { Err(DomainError::InvalidQuery(msg)) => { assert!( - msg.contains("gts.*.*.rg.*"), + msg.contains(&invalid_pattern), "msg should cite the input: {msg}" ); } @@ -703,7 +716,7 @@ mod tests { let repo = InMemoryGtsRepository::new(default_config()); let entity = json!({ - "$id": "gts://gts.acme.core.events.user_created.v1~", + "$id": schema_uri(USER_CREATED_ID), "$schema": JSON_SCHEMA_DRAFT_07, "type": "object", "description": "A user created event" @@ -718,7 +731,7 @@ mod tests { let repo = InMemoryGtsRepository::new(default_config()); let entity = json!({ - "id": "gts.acme.core.events.user_created.v1~acme.core.events.instance.v1", + "id": gts_id!("acme.core.events.user_created.v1~acme.core.events.instance.v1"), "data": "value" }); @@ -731,7 +744,7 @@ mod tests { let repo = InMemoryGtsRepository::new(default_config()); let entity = json!({ - "gtsId": "gts.acme.core.events.user_created.v1~", + "gtsId": USER_CREATED_ID, "$schema": JSON_SCHEMA_DRAFT_07, "type": "object" }); @@ -745,7 +758,7 @@ mod tests { let repo = InMemoryGtsRepository::new(default_config()); let entity = json!({ - "id": "gts.acme.core.events.user_created.v1~", + "id": USER_CREATED_ID, "$schema": JSON_SCHEMA_DRAFT_07, "type": "object" }); diff --git a/gears/system/types-registry/types-registry/tests/edge_cases_tests.rs b/gears/system/types-registry/types-registry/tests/edge_cases_tests.rs index ffc1fa3de..6ff427f2d 100644 --- a/gears/system/types-registry/types-registry/tests/edge_cases_tests.rs +++ b/gears/system/types-registry/types-registry/tests/edge_cases_tests.rs @@ -6,6 +6,7 @@ mod common; use common::create_service; use serde_json::json; +use toolkit_gts::{GTS_ID_PREFIX, GTS_ID_URI_PREFIX, gts_id, gts_uri}; // ============================================================================= // Error Handling Tests @@ -16,7 +17,7 @@ async fn test_get_nonexistent_entity() { let service = create_service(); service.switch_to_ready().unwrap(); - let result = service.get("gts.nonexistent.pkg.ns.type.v1~"); + let result = service.get(gts_id!("nonexistent.pkg.ns.type.v1~")); assert!(result.is_err()); } @@ -27,8 +28,8 @@ async fn test_invalid_gts_id_formats() { let invalid_entities = vec![ json!({ "$id": "not-a-gts-id", "type": "object" }), json!({ "$id": "gts", "type": "object" }), - json!({ "$id": "gts.vendor", "type": "object" }), - json!({ "$id": "gts://gts.vendor", "type": "object" }), + json!({ "$id": format!("{GTS_ID_PREFIX}vendor"), "type": "object" }), + json!({ "$id": format!("{GTS_ID_URI_PREFIX}{GTS_ID_PREFIX}vendor"), "type": "object" }), json!({ "$id": "", "type": "object" }), ]; @@ -51,7 +52,7 @@ async fn test_gts_id_extraction_priority() { // Test that $id field is used when present (highest priority) // The repository extracts GTS ID from configured fields in order let entity = json!({ - "$id": "gts://gts.acme.core.events.from_dollar_id.v1~", + "$id": gts_uri!("acme.core.events.from_dollar_id.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object" }); @@ -62,11 +63,11 @@ async fn test_gts_id_extraction_priority() { service.switch_to_ready().unwrap(); // Verify the entity was registered with the $id value - let retrieved = service.get("gts.acme.core.events.from_dollar_id.v1~"); + let retrieved = service.get(gts_id!("acme.core.events.from_dollar_id.v1~")); assert!(retrieved.is_ok()); assert_eq!( retrieved.unwrap().gts_id, - "gts.acme.core.events.from_dollar_id.v1~" + gts_id!("acme.core.events.from_dollar_id.v1~") ); } @@ -79,7 +80,7 @@ async fn test_entity_content_preserved() { let service = create_service(); let original_content = json!({ - "$id": "gts://gts.acme.core.events.content_test.v1~", + "$id": gts_uri!("acme.core.events.content_test.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { @@ -102,7 +103,7 @@ async fn test_entity_content_preserved() { service.switch_to_ready().unwrap(); let retrieved = service - .get("gts.acme.core.events.content_test.v1~") + .get(gts_id!("acme.core.events.content_test.v1~")) .unwrap(); // Verify description is extracted @@ -121,7 +122,7 @@ async fn test_entity_segments_parsed_correctly() { let service = create_service(); let entity = json!({ - "$id": "gts://gts.myvendor.mypackage.mynamespace.mytype.v2~", + "$id": gts_uri!("myvendor.mypackage.mynamespace.mytype.v2~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object" }); @@ -130,7 +131,7 @@ async fn test_entity_segments_parsed_correctly() { service.switch_to_ready().unwrap(); let retrieved = service - .get("gts.myvendor.mypackage.mynamespace.mytype.v2~") + .get(gts_id!("myvendor.mypackage.mynamespace.mytype.v2~")) .unwrap(); assert_eq!(retrieved.vendor(), Some("myvendor")); @@ -150,12 +151,12 @@ async fn test_gts_id_with_special_segments() { // GTS IDs with underscores and numbers let entities = vec![ json!({ - "$id": "gts://gts.acme_corp.core_v2.events_ns.my_type_123.v1~", + "$id": gts_uri!("acme_corp.core_v2.events_ns.my_type_123.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object" }), json!({ - "$id": "gts://gts.vendor123.pkg456.ns789.type000.v99~", + "$id": gts_uri!("vendor123.pkg456.ns789.type000.v99~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object" }), @@ -175,6 +176,6 @@ async fn test_gts_id_with_special_segments() { service.switch_to_ready().unwrap(); - let e1 = service.get("gts.acme_corp.core_v2.events_ns.my_type_123.v1~"); + let e1 = service.get(gts_id!("acme_corp.core_v2.events_ns.my_type_123.v1~")); assert!(e1.is_ok()); } diff --git a/gears/system/types-registry/types-registry/tests/query_tests.rs b/gears/system/types-registry/types-registry/tests/query_tests.rs index de78b0151..b3ab2c437 100644 --- a/gears/system/types-registry/types-registry/tests/query_tests.rs +++ b/gears/system/types-registry/types-registry/tests/query_tests.rs @@ -7,6 +7,8 @@ mod common; use axum::extract::Json; use common::create_service; use serde_json::json; +use toolkit_gts::gts_uri; +use toolkit_gts::{GTS_ID_PREFIX, gts_id}; use types_registry::api::rest::dto::ListEntitiesQuery; use types_registry::domain::model::ListQuery; @@ -19,10 +21,10 @@ async fn test_list_with_pattern_filter() { let service = create_service(); let entities = vec![ - json!({ "$id": "gts://gts.acme.core.events.user_created.v1~", "$schema": "http://json-schema.org/draft-07/schema#", "type": "object" }), - json!({ "$id": "gts://gts.acme.core.events.user_updated.v1~", "$schema": "http://json-schema.org/draft-07/schema#", "type": "object" }), - json!({ "$id": "gts://gts.acme.core.events.order_created.v1~", "$schema": "http://json-schema.org/draft-07/schema#", "type": "object" }), - json!({ "$id": "gts://gts.globex.core.events.shipment.v1~", "$schema": "http://json-schema.org/draft-07/schema#", "type": "object" }), + json!({ "$id": gts_uri!("acme.core.events.user_created.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object" }), + json!({ "$id": gts_uri!("acme.core.events.user_updated.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object" }), + json!({ "$id": gts_uri!("acme.core.events.order_created.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object" }), + json!({ "$id": gts_uri!("globex.core.events.shipment.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object" }), ]; _ = service.register(entities); @@ -30,20 +32,20 @@ async fn test_list_with_pattern_filter() { // Wildcard limited to one vendor. let acme = service - .list(&ListQuery::default().with_pattern("gts.acme.*")) + .list(&ListQuery::default().with_pattern(format!("{GTS_ID_PREFIX}acme.*"))) .unwrap(); assert_eq!(acme.len(), 3); assert!(acme.iter().all(|e| e.vendor() == Some("acme"))); // Tighter wildcard: vendor + package. let acme_core = service - .list(&ListQuery::default().with_pattern("gts.acme.core.*")) + .list(&ListQuery::default().with_pattern(format!("{GTS_ID_PREFIX}acme.core.*"))) .unwrap(); assert_eq!(acme_core.len(), 3); // No matches. let none = service - .list(&ListQuery::default().with_pattern("gts.nope.*")) + .list(&ListQuery::default().with_pattern(format!("{GTS_ID_PREFIX}nope.*"))) .unwrap(); assert!(none.is_empty()); } @@ -53,7 +55,7 @@ async fn test_list_with_is_type_filter() { let service = create_service(); let type_schema = json!({ - "$id": "gts://gts.acme.core.models.filter_test.v1~", + "$id": gts_uri!("acme.core.models.filter_test.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { "name": { "type": "string" } } @@ -64,11 +66,11 @@ async fn test_list_with_is_type_filter() { let instances = vec![ json!({ - "id": "gts.acme.core.models.filter_test.v1~acme.core.instances.i1.v1", + "id": gts_id!("acme.core.models.filter_test.v1~acme.core.instances.i1.v1"), "name": "instance1" }), json!({ - "id": "gts.acme.core.models.filter_test.v1~acme.core.instances.i2.v1", + "id": gts_id!("acme.core.models.filter_test.v1~acme.core.instances.i2.v1"), "name": "instance2" }), ]; @@ -106,13 +108,13 @@ async fn test_rest_list_handler_integration() { let service = create_service(); _ = service.register(vec![ - json!({ "$id": "gts://gts.acme.core.events.list_test1.v1~", "$schema": "http://json-schema.org/draft-07/schema#", "type": "object" }), - json!({ "$id": "gts://gts.acme.core.events.list_test2.v1~", "$schema": "http://json-schema.org/draft-07/schema#", "type": "object" }), + json!({ "$id": gts_uri!("acme.core.events.list_test1.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object" }), + json!({ "$id": gts_uri!("acme.core.events.list_test2.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object" }), ]); service.switch_to_ready().unwrap(); let query = ListEntitiesQuery { - pattern: Some("gts.acme.*".to_owned()), + pattern: Some(format!("{GTS_ID_PREFIX}acme.*")), ..Default::default() }; @@ -132,7 +134,7 @@ async fn test_rest_list_empty_results() { service.switch_to_ready().unwrap(); let query = ListEntitiesQuery { - pattern: Some("gts.nonexistent.*".to_owned()), + pattern: Some(format!("{GTS_ID_PREFIX}nonexistent.*")), ..Default::default() }; @@ -156,7 +158,7 @@ async fn test_rest_get_handler_integration() { let service = create_service(); _ = service.register(vec![json!({ - "$id": "gts://gts.acme.core.events.get_test.v1~", + "$id": gts_uri!("acme.core.events.get_test.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "description": "Test entity for GET handler" @@ -165,13 +167,13 @@ async fn test_rest_get_handler_integration() { let result = get_entity( Extension(service), - Path("gts.acme.core.events.get_test.v1~".to_owned()), + Path(gts_id!("acme.core.events.get_test.v1~").to_owned()), ) .await; assert!(result.is_ok()); let Json(entity) = result.unwrap(); - assert_eq!(entity.gts_id, "gts.acme.core.events.get_test.v1~"); + assert_eq!(entity.gts_id, gts_id!("acme.core.events.get_test.v1~")); assert_eq!( entity.description, Some("Test entity for GET handler".to_owned()) @@ -188,7 +190,7 @@ async fn test_rest_get_handler_not_found() { let result = get_entity( Extension(service), - Path("gts.nonexistent.pkg.ns.type.v1~".to_owned()), + Path(gts_id!("nonexistent.pkg.ns.type.v1~").to_owned()), ) .await; diff --git a/gears/system/types-registry/types-registry/tests/ready_mode_tests.rs b/gears/system/types-registry/types-registry/tests/ready_mode_tests.rs index ceeea2782..8dc88b515 100644 --- a/gears/system/types-registry/types-registry/tests/ready_mode_tests.rs +++ b/gears/system/types-registry/types-registry/tests/ready_mode_tests.rs @@ -6,6 +6,7 @@ mod common; use common::create_service; use serde_json::json; +use toolkit_gts::{GTS_ID_PREFIX, gts_id, gts_uri}; use types_registry::domain::model::ListQuery; // ============================================================================= @@ -24,7 +25,7 @@ async fn test_ready_mode_validates_immediately_with_correct_order() { let entities = vec![ // Parent type - must be first for instances to validate against it json!({ - "$id": "gts://gts.acme.core.models.person.v1~", + "$id": gts_uri!("acme.core.models.person.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { @@ -36,19 +37,19 @@ async fn test_ready_mode_validates_immediately_with_correct_order() { }), // Instance 1 - valid, conforms to schema json!({ - "id": "gts.acme.core.models.person.v1~acme.core.instances.person1.v1", + "id": gts_id!("acme.core.models.person.v1~acme.core.instances.person1.v1"), "name": "Alice", "age": 30 }), // Instance 2 - valid json!({ - "id": "gts.acme.core.models.person.v1~acme.core.instances.person2.v1", + "id": gts_id!("acme.core.models.person.v1~acme.core.instances.person2.v1"), "name": "Bob", "age": 25 }), // Instance 3 - valid json!({ - "id": "gts.acme.core.models.person.v1~acme.core.instances.person3.v1", + "id": gts_id!("acme.core.models.person.v1~acme.core.instances.person3.v1"), "name": "Charlie", "age": 35 }), @@ -90,13 +91,13 @@ async fn test_ready_mode_fails_when_instance_before_parent() { let entities = vec![ // Instance first - will fail because parent doesn't exist yet json!({ - "id": "gts.acme.core.models.widget.v1~acme.core.instances.widget1.v1", + "id": gts_id!("acme.core.models.widget.v1~acme.core.instances.widget1.v1"), "widgetId": "w-001", "color": "red" }), // Parent type - registered after instance json!({ - "$id": "gts://gts.acme.core.models.widget.v1~", + "$id": gts_uri!("acme.core.models.widget.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { @@ -136,7 +137,7 @@ async fn test_ready_mode_validates_invalid_instance_immediately() { let entities = vec![ // Parent type json!({ - "$id": "gts://gts.acme.core.models.employee.v1~", + "$id": gts_uri!("acme.core.models.employee.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { @@ -149,7 +150,7 @@ async fn test_ready_mode_validates_invalid_instance_immediately() { }), // Invalid instance - missing required "salary" field json!({ - "id": "gts.acme.core.models.employee.v1~acme.core.instances.emp1.v1", + "id": gts_id!("acme.core.models.employee.v1~acme.core.instances.emp1.v1"), "employeeId": "emp-001", "department": "Engineering" // Missing required "salary" field @@ -184,7 +185,7 @@ async fn test_ready_mode_batch_with_valid_and_invalid_instances() { let entities = vec![ // Parent type json!({ - "$id": "gts://gts.acme.core.models.item.v1~", + "$id": gts_uri!("acme.core.models.item.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { @@ -196,19 +197,19 @@ async fn test_ready_mode_batch_with_valid_and_invalid_instances() { }), // Valid instance json!({ - "id": "gts.acme.core.models.item.v1~acme.core.instances.item1.v1", + "id": gts_id!("acme.core.models.item.v1~acme.core.instances.item1.v1"), "itemId": "item-001", "price": 29.99 }), // Invalid instance - wrong type for price json!({ - "id": "gts.acme.core.models.item.v1~acme.core.instances.item2.v1", + "id": gts_id!("acme.core.models.item.v1~acme.core.instances.item2.v1"), "itemId": "item-002", "price": "not-a-number" // Should be number }), // Another valid instance json!({ - "id": "gts.acme.core.models.item.v1~acme.core.instances.item3.v1", + "id": gts_id!("acme.core.models.item.v1~acme.core.instances.item3.v1"), "itemId": "item-003", "price": 49.99 }), @@ -244,7 +245,7 @@ async fn test_configuration_mode_defers_validation() { // Register a type schema let type_schema = json!({ - "$id": "gts://gts.acme.core.models.config_test.v1~", + "$id": gts_uri!("acme.core.models.config_test.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { @@ -258,7 +259,7 @@ async fn test_configuration_mode_defers_validation() { // Register an instance that would fail validation (missing required field) // In configuration mode, this should succeed (deferred validation) let invalid_instance = json!({ - "id": "gts.acme.core.models.config_test.v1~acme.core.instances.test1.v1" + "id": gts_id!("acme.core.models.config_test.v1~acme.core.instances.test1.v1") // Missing "requiredField" }); @@ -278,12 +279,12 @@ async fn test_switch_to_production_validates_all_entities() { // Register valid entities in configuration mode let entities = vec![ json!({ - "$id": "gts://gts.acme.core.events.valid1.v1~", + "$id": gts_uri!("acme.core.events.valid1.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object" }), json!({ - "$id": "gts://gts.acme.core.events.valid2.v1~", + "$id": gts_uri!("acme.core.events.valid2.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object" }), @@ -310,7 +311,7 @@ async fn test_switch_to_ready_is_idempotent() { // Register something first _ = service.register(vec![json!({ - "$id": "gts://gts.acme.core.events.state_test.v1~", + "$id": gts_uri!("acme.core.events.state_test.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object" })]); @@ -332,7 +333,7 @@ async fn test_list_before_ready_returns_empty() { // Register entities in configuration mode _ = service.register(vec![json!({ - "$id": "gts://gts.acme.core.events.not_visible.v1~", + "$id": gts_uri!("acme.core.events.not_visible.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object" })]); @@ -348,13 +349,13 @@ async fn test_get_before_ready_fails() { // Register entity in configuration mode _ = service.register(vec![json!({ - "$id": "gts://gts.acme.core.events.not_accessible.v1~", + "$id": gts_uri!("acme.core.events.not_accessible.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object" })]); // Get should fail before switching to ready - let result = service.get("gts.acme.core.events.not_accessible.v1~"); + let result = service.get(gts_id!("acme.core.events.not_accessible.v1~")); assert!(result.is_err(), "Get should fail before ready mode"); } @@ -364,11 +365,12 @@ async fn test_get_before_ready_fails() { #[tokio::test] async fn test_switch_to_ready_returns_errors_as_list() { + const PRODUCT_ID: &str = gts_id!("acme.core.models.product.v1~"); let service = create_service(); // Register a parent type schema let parent_type = json!({ - "$id": "gts://gts.acme.core.models.product.v1~", + "$id": gts_uri!("acme.core.models.product.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { @@ -384,13 +386,13 @@ async fn test_switch_to_ready_returns_errors_as_list() { // Register multiple invalid child instances in configuration mode // Child 1: Missing all required fields let invalid_child1 = json!({ - "id": "gts.acme.core.models.product.v1~acme.core.instances.product1.v1" + "id": gts_id!("acme.core.models.product.v1~acme.core.instances.product1.v1") // Missing productId, name, price }); // Child 2: Missing price field let invalid_child2 = json!({ - "id": "gts.acme.core.models.product.v1~acme.core.instances.product2.v1", + "id": gts_id!("acme.core.models.product.v1~acme.core.instances.product2.v1"), "productId": "prod-002", "name": "Widget" // Missing price @@ -398,7 +400,7 @@ async fn test_switch_to_ready_returns_errors_as_list() { // Child 3: Wrong type for price (string instead of number) let invalid_child3 = json!({ - "id": "gts.acme.core.models.product.v1~acme.core.instances.product3.v1", + "id": gts_id!("acme.core.models.product.v1~acme.core.instances.product3.v1"), "productId": "prod-003", "name": "Gadget", "price": "not-a-number" // Should be number @@ -445,7 +447,7 @@ async fn test_switch_to_ready_returns_errors_as_list() { // Each error should have the GTS ID of the failing entity for err in errors { assert!( - err.gts_id.contains("gts.acme.core.models.product.v1~"), + err.gts_id.contains(PRODUCT_ID), "Error should contain GTS ID: {err:?}" ); } @@ -464,11 +466,13 @@ async fn test_switch_to_ready_returns_errors_as_list() { #[tokio::test] async fn test_switch_to_ready_error_contains_gts_ids() { + const MISSING_NAME_ID: &str = + gts_id!("acme.core.models.error_test.v1~acme.core.instances.missing_name.v1"); let service = create_service(); // Register a type schema let type_schema = json!({ - "$id": "gts://gts.acme.core.models.error_test.v1~", + "$id": gts_uri!("acme.core.models.error_test.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { @@ -481,7 +485,7 @@ async fn test_switch_to_ready_error_contains_gts_ids() { // Register an invalid instance let invalid_instance = json!({ - "id": "gts.acme.core.models.error_test.v1~acme.core.instances.missing_name.v1" + "id": MISSING_NAME_ID // Missing required "name" field }); @@ -503,8 +507,7 @@ async fn test_switch_to_ready_error_contains_gts_ids() { // The error should contain the GTS ID of the failing entity let err = &errors[0]; assert!( - err.gts_id - .contains("gts.acme.core.models.error_test.v1~acme.core.instances.missing_name.v1"), + err.gts_id.contains(MISSING_NAME_ID), "Error gts_id should reference the failing entity: {err:?}" ); assert!( @@ -519,7 +522,7 @@ async fn test_switch_to_ready_success_with_valid_types_only() { // Register only valid type schemas (no instances that need validation) let type_schema1 = json!({ - "$id": "gts://gts.acme.core.models.fixable1.v1~", + "$id": gts_uri!("acme.core.models.fixable1.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { @@ -528,7 +531,7 @@ async fn test_switch_to_ready_success_with_valid_types_only() { }); let type_schema2 = json!({ - "$id": "gts://gts.acme.core.models.fixable2.v1~", + "$id": gts_uri!("acme.core.models.fixable2.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { @@ -565,8 +568,9 @@ async fn test_concurrent_registrations() { for i in 0..10 { let svc = service.clone(); let handle = tokio::spawn(async move { + let gts_id = format!("{GTS_ID_PREFIX}acme.core.events.concurrent_{i}.v1~"); let entity = json!({ - "$id": format!("gts://gts.acme.core.events.concurrent_{i}.v1~"), + "$id": gts_uri!(gts_id.as_str()), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object" }); diff --git a/gears/system/types-registry/types-registry/tests/registration_tests.rs b/gears/system/types-registry/types-registry/tests/registration_tests.rs index 40f7a0341..10b66e83d 100644 --- a/gears/system/types-registry/types-registry/tests/registration_tests.rs +++ b/gears/system/types-registry/types-registry/tests/registration_tests.rs @@ -7,6 +7,7 @@ mod common; use axum::http::StatusCode; use common::create_service; use serde_json::json; +use toolkit_gts::{GTS_ID_PREFIX, gts_id, gts_uri}; use types_registry::api::rest::dto::RegisterEntitiesRequest; use types_registry::domain::model::ListQuery; @@ -47,7 +48,7 @@ async fn test_batch_with_anonymous_entities_rejected_immediately() { // Mix of valid and anonymous entities let valid_entity = json!({ - "$id": "gts://gts.acme.core.models.valid.v1~", + "$id": gts_uri!("acme.core.models.valid.v1~"), "type": "object", "properties": { "name": { "type": "string" } @@ -82,7 +83,7 @@ async fn test_anonymous_entity_rejected_in_ready_mode() { // Register a valid entity first and switch to ready let valid_entity = json!({ - "$id": "gts://gts.acme.core.models.setup.v1~", + "$id": gts_uri!("acme.core.models.setup.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object" }); @@ -118,7 +119,7 @@ async fn test_full_registration_flow_configuration_to_ready() { assert!(!service.is_ready()); let type_schema = json!({ - "$id": "gts://gts.acme.core.events.user_created.v1~", + "$id": gts_uri!("acme.core.events.user_created.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { @@ -141,9 +142,9 @@ async fn test_full_registration_flow_configuration_to_ready() { // Phase 3: Verify entity is accessible let entity = service - .get("gts.acme.core.events.user_created.v1~") + .get(gts_id!("acme.core.events.user_created.v1~")) .unwrap(); - assert_eq!(entity.gts_id, "gts.acme.core.events.user_created.v1~"); + assert_eq!(entity.gts_id, gts_id!("acme.core.events.user_created.v1~")); assert!(entity.is_type()); assert_eq!(entity.vendor(), Some("acme")); assert_eq!(entity.package(), Some("core")); @@ -156,7 +157,7 @@ async fn test_batch_registration_with_mixed_results() { let entities = vec![ json!({ - "$id": "gts://gts.acme.core.events.valid_type.v1~", + "$id": gts_uri!("acme.core.events.valid_type.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object" }), @@ -165,7 +166,7 @@ async fn test_batch_registration_with_mixed_results() { "type": "object" }), json!({ - "$id": "gts://gts.globex.core.events.another_valid.v1~", + "$id": gts_uri!("globex.core.events.another_valid.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object" }), @@ -195,13 +196,13 @@ async fn test_duplicate_registration_fails() { let service = create_service(); let entity = json!({ - "$id": "gts://gts.acme.core.events.user_created.v1~", + "$id": gts_uri!("acme.core.events.user_created.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object" }); let entity_modified = json!({ - "$id": "gts://gts.acme.core.events.user_created.v1~", + "$id": gts_uri!("acme.core.events.user_created.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "description": "modified" @@ -226,7 +227,7 @@ async fn test_registration_in_ready_mode() { // Register in ready mode (with validation) let entity = json!({ - "$id": "gts://gts.acme.core.events.ready_type.v1~", + "$id": gts_uri!("acme.core.events.ready_type.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { @@ -238,7 +239,7 @@ async fn test_registration_in_ready_mode() { assert!(results[0].is_ok()); // Entity should be immediately accessible - let retrieved = service.get("gts.acme.core.events.ready_type.v1~"); + let retrieved = service.get(gts_id!("acme.core.events.ready_type.v1~")); assert!(retrieved.is_ok()); } @@ -257,8 +258,9 @@ async fn test_large_batch_registration() { // Register 100 entities in a single batch let entities: Vec<_> = (0..100) .map(|i| { + let gts_id = format!("{GTS_ID_PREFIX}acme.core.events.batch_{i}.v1~"); json!({ - "$id": format!("gts://gts.acme.core.events.batch_{i}.v1~"), + "$id": gts_uri!(gts_id.as_str()), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object" }) @@ -290,7 +292,7 @@ async fn test_rest_register_handler_integration() { let request = RegisterEntitiesRequest { entities: vec![json!({ - "$id": "gts://gts.acme.core.events.rest_test.v1~", + "$id": gts_uri!("acme.core.events.rest_test.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "description": "Test type for REST handler" diff --git a/gears/system/types-registry/types-registry/tests/rg_gts_type_system_tests.rs b/gears/system/types-registry/types-registry/tests/rg_gts_type_system_tests.rs index 5b1d7ae1c..d7a1cc092 100644 --- a/gears/system/types-registry/types-registry/tests/rg_gts_type_system_tests.rs +++ b/gears/system/types-registry/types-registry/tests/rg_gts_type_system_tests.rs @@ -13,6 +13,7 @@ mod common; use common::create_service; use serde_json::json; +use toolkit_gts::{GTS_ID_PREFIX, GTS_ID_URI_PREFIX, gts_id, gts_uri}; use types_registry::domain::model::ListQuery; // ============================================================================= @@ -23,7 +24,7 @@ use types_registry::domain::model::ListQuery; /// Closed model (additionalProperties:false) with metadata placeholder. fn rg_base_contract() -> serde_json::Value { json!({ - "$id": "gts://gts.cf.core.rg.type.v1~", + "$id": gts_uri!("cf.core.rg.type.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "title": "Resource Group Type", "type": "object", @@ -37,12 +38,12 @@ fn rg_base_contract() -> serde_json::Value { }, "allowed_parent_types": { "type": "array", - "items": { "type": "string", "x-gts-ref": "gts.cf.core.rg.type.v1~" }, + "items": { "type": "string", "x-gts-ref": gts_id!("cf.core.rg.type.v1~") }, "default": [] }, "allowed_membership_types": { "type": "array", - "items": { "type": "string", "x-gts-ref": "gts.*" }, + "items": { "type": "string", "x-gts-ref": format!("{GTS_ID_PREFIX}*") }, "default": [] } } @@ -70,7 +71,7 @@ fn rg_base_contract() -> serde_json::Value { /// Tenant entity schema — gts.cf.core.tn.tenant.v1~ (registered for reference, not $ref'd) fn tenant_entity_schema() -> serde_json::Value { json!({ - "$id": "gts://gts.cf.core.tn.tenant.v1~", + "$id": gts_uri!("cf.core.tn.tenant.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "title": "Tenant", "type": "object", @@ -87,7 +88,7 @@ fn tenant_entity_schema() -> serde_json::Value { /// Department entity schema — gts.cf.core.org.department.v1~ (registered for reference) fn department_entity_schema() -> serde_json::Value { json!({ - "$id": "gts://gts.cf.core.org.department.v1~", + "$id": gts_uri!("cf.core.org.department.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "title": "Department", "type": "object", @@ -103,7 +104,7 @@ fn department_entity_schema() -> serde_json::Value { /// Branch entity schema — gts.cf.core.rg.branch.v1~ (registered for reference) fn branch_entity_schema() -> serde_json::Value { json!({ - "$id": "gts://gts.cf.core.rg.branch.v1~", + "$id": gts_uri!("cf.core.rg.branch.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "title": "Branch", "type": "object", @@ -119,7 +120,7 @@ fn branch_entity_schema() -> serde_json::Value { /// User resource schema — gts.cf.core.idp.user.v1~ fn user_resource_schema() -> serde_json::Value { json!({ - "$id": "gts://gts.cf.core.idp.user.v1~", + "$id": gts_uri!("cf.core.idp.user.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "title": "User", "type": "object", @@ -136,7 +137,7 @@ fn user_resource_schema() -> serde_json::Value { /// Course resource schema — gts.cf.core.lms.course.v1~ fn course_resource_schema() -> serde_json::Value { json!({ - "$id": "gts://gts.cf.core.lms.course.v1~", + "$id": gts_uri!("cf.core.lms.course.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "title": "Course", "type": "object", @@ -151,15 +152,15 @@ fn course_resource_schema() -> serde_json::Value { /// Tenant as RG type — single $ref + inline metadata fn tenant_rg_type() -> serde_json::Value { json!({ - "$id": "gts://gts.cf.core.rg.type.v1~y.core.tn.tenant.v1~", + "$id": gts_uri!("cf.core.rg.type.v1~y.core.tn.tenant.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "x-gts-traits": { "can_be_root": true, - "allowed_parent_types": ["gts.cf.core.rg.type.v1~y.core.tn.tenant.v1~"], - "allowed_membership_types": ["gts.cf.core.idp.user.v1~"] + "allowed_parent_types": [gts_id!("cf.core.rg.type.v1~y.core.tn.tenant.v1~")], + "allowed_membership_types": [gts_id!("cf.core.idp.user.v1~")] }, "allOf": [ - { "$ref": "gts://gts.cf.core.rg.type.v1~" }, + { "$ref": gts_uri!("cf.core.rg.type.v1~") }, { "properties": { "metadata": { @@ -179,15 +180,15 @@ fn tenant_rg_type() -> serde_json::Value { /// Department as RG type — single $ref + inline metadata fn department_rg_type() -> serde_json::Value { json!({ - "$id": "gts://gts.cf.core.rg.type.v1~w.core.org.department.v1~", + "$id": gts_uri!("cf.core.rg.type.v1~w.core.org.department.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "x-gts-traits": { "can_be_root": false, - "allowed_parent_types": ["gts.cf.core.rg.type.v1~y.core.tn.tenant.v1~"], - "allowed_membership_types": ["gts.cf.core.idp.user.v1~"] + "allowed_parent_types": [gts_id!("cf.core.rg.type.v1~y.core.tn.tenant.v1~")], + "allowed_membership_types": [gts_id!("cf.core.idp.user.v1~")] }, "allOf": [ - { "$ref": "gts://gts.cf.core.rg.type.v1~" }, + { "$ref": gts_uri!("cf.core.rg.type.v1~") }, { "properties": { "metadata": { @@ -207,18 +208,18 @@ fn department_rg_type() -> serde_json::Value { /// Branch as RG type — single $ref + inline metadata fn branch_rg_type() -> serde_json::Value { json!({ - "$id": "gts://gts.cf.core.rg.type.v1~cf.core.rg.branch.v1~", + "$id": gts_uri!("cf.core.rg.type.v1~cf.core.rg.branch.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "x-gts-traits": { "can_be_root": false, - "allowed_parent_types": ["gts.cf.core.rg.type.v1~w.core.org.department.v1~"], + "allowed_parent_types": [gts_id!("cf.core.rg.type.v1~w.core.org.department.v1~")], "allowed_membership_types": [ - "gts.cf.core.idp.user.v1~", - "gts.cf.core.lms.course.v1~" + gts_id!("cf.core.idp.user.v1~"), + gts_id!("cf.core.lms.course.v1~") ] }, "allOf": [ - { "$ref": "gts://gts.cf.core.rg.type.v1~" }, + { "$ref": gts_uri!("cf.core.rg.type.v1~") }, { "properties": { "metadata": { @@ -287,7 +288,7 @@ async fn test_all_rg_schemas_register_and_validate() { #[tokio::test] async fn test_base_rg_contract_has_metadata_property() { let service = setup_rg_type_system(); - let rg_base = service.get("gts.cf.core.rg.type.v1~").unwrap(); + let rg_base = service.get(gts_id!("cf.core.rg.type.v1~")).unwrap(); assert!(rg_base.is_type()); assert_eq!(rg_base.vendor(), Some("cf")); @@ -300,7 +301,7 @@ async fn test_base_rg_contract_has_metadata_property() { async fn test_chained_tenant_has_inline_metadata_schema() { let service = setup_rg_type_system(); let tenant_rg = service - .get("gts.cf.core.rg.type.v1~y.core.tn.tenant.v1~") + .get(gts_id!("cf.core.rg.type.v1~y.core.tn.tenant.v1~")) .unwrap(); assert!(tenant_rg.is_type()); assert_eq!(tenant_rg.segments.len(), 2); @@ -324,13 +325,13 @@ async fn test_chained_tenant_has_inline_metadata_schema() { async fn test_chained_department_cannot_be_root() { let service = setup_rg_type_system(); let dept_rg = service - .get("gts.cf.core.rg.type.v1~w.core.org.department.v1~") + .get(gts_id!("cf.core.rg.type.v1~w.core.org.department.v1~")) .unwrap(); let traits = &dept_rg.content["x-gts-traits"]; assert_eq!(traits["can_be_root"], json!(false)); assert_eq!( traits["allowed_parent_types"], - json!(["gts.cf.core.rg.type.v1~y.core.tn.tenant.v1~"]) + json!([gts_id!("cf.core.rg.type.v1~y.core.tn.tenant.v1~")]) ); } @@ -338,14 +339,14 @@ async fn test_chained_department_cannot_be_root() { async fn test_branch_allows_users_and_courses_as_members() { let service = setup_rg_type_system(); let branch_rg = service - .get("gts.cf.core.rg.type.v1~cf.core.rg.branch.v1~") + .get(gts_id!("cf.core.rg.type.v1~cf.core.rg.branch.v1~")) .unwrap(); let memberships = branch_rg.content["x-gts-traits"]["allowed_membership_types"] .as_array() .unwrap(); assert_eq!(memberships.len(), 2); - assert!(memberships.contains(&json!("gts.cf.core.idp.user.v1~"))); - assert!(memberships.contains(&json!("gts.cf.core.lms.course.v1~"))); + assert!(memberships.contains(&json!(gts_id!("cf.core.idp.user.v1~")))); + assert!(memberships.contains(&json!(gts_id!("cf.core.lms.course.v1~")))); } // ============================================================================= @@ -357,26 +358,29 @@ async fn test_vendor_isolation_across_rg_types() { let service = setup_rg_type_system(); assert!( service - .list(&ListQuery::default().with_pattern("gts.cf.*")) + .list(&ListQuery::default().with_pattern(format!("{GTS_ID_PREFIX}cf.*"))) .unwrap() .len() >= 3 ); assert!( !service - .list(&ListQuery::default().with_pattern("gts.cf.*")) + .list(&ListQuery::default().with_pattern(format!("{GTS_ID_PREFIX}cf.*"))) .unwrap() .is_empty() ); assert!( !service - .list(&ListQuery::default().with_pattern("gts.cf.core.rg.type.v1~w.*")) + .list( + &ListQuery::default() + .with_pattern(format!("{GTS_ID_PREFIX}cf.core.rg.type.v1~w.*")) + ) .unwrap() .is_empty() ); assert!( service - .list(&ListQuery::default().with_pattern("gts.cf.*")) + .list(&ListQuery::default().with_pattern(format!("{GTS_ID_PREFIX}cf.*"))) .unwrap() .len() >= 2 @@ -391,7 +395,7 @@ async fn test_vendor_isolation_across_rg_types() { async fn test_valid_tenant_root_no_metadata() { let service = setup_rg_type_system(); let t1 = json!({ - "id": "gts.cf.core.rg.type.v1~y.core.tn.tenant.v1~cf.core._.t1.v1", + "id": gts_id!("cf.core.rg.type.v1~y.core.tn.tenant.v1~cf.core._.t1.v1"), "name": "T1", "parent_id": null, "tenant_id": "11111111-1111-1111-1111-111111111111", @@ -409,7 +413,7 @@ async fn test_valid_tenant_root_no_metadata() { async fn test_valid_tenant_with_metadata_custom_domain() { let service = setup_rg_type_system(); let t9 = json!({ - "id": "gts.cf.core.rg.type.v1~y.core.tn.tenant.v1~cf.core._.t9.v1", + "id": gts_id!("cf.core.rg.type.v1~y.core.tn.tenant.v1~cf.core._.t9.v1"), "name": "T9", "parent_id": null, "tenant_id": "99999999-9999-9999-9999-999999999999", @@ -428,7 +432,7 @@ async fn test_valid_tenant_with_metadata_custom_domain() { async fn test_valid_tenant_with_metadata_barrier() { let service = setup_rg_type_system(); let t7 = json!({ - "id": "gts.cf.core.rg.type.v1~y.core.tn.tenant.v1~cf.core._.t7.v1", + "id": gts_id!("cf.core.rg.type.v1~y.core.tn.tenant.v1~cf.core._.t7.v1"), "name": "T7", "parent_id": "11111111-1111-1111-1111-111111111111", "tenant_id": "77777777-7777-7777-7777-777777777777", @@ -447,7 +451,7 @@ async fn test_valid_tenant_with_metadata_barrier() { async fn test_valid_department_with_metadata() { let service = setup_rg_type_system(); let d2 = json!({ - "id": "gts.cf.core.rg.type.v1~w.core.org.department.v1~cf.core._.d2.v1", + "id": gts_id!("cf.core.rg.type.v1~w.core.org.department.v1~cf.core._.d2.v1"), "name": "D2", "parent_id": "11111111-1111-1111-1111-111111111111", "tenant_id": "11111111-1111-1111-1111-111111111111", @@ -466,7 +470,7 @@ async fn test_valid_department_with_metadata() { async fn test_valid_branch_with_metadata() { let service = setup_rg_type_system(); let b3 = json!({ - "id": "gts.cf.core.rg.type.v1~cf.core.rg.branch.v1~cf.core._.b3.v1", + "id": gts_id!("cf.core.rg.type.v1~cf.core.rg.branch.v1~cf.core._.b3.v1"), "name": "B3", "parent_id": "22222222-2222-2222-2222-222222222222", "tenant_id": "11111111-1111-1111-1111-111111111111", @@ -485,7 +489,7 @@ async fn test_valid_branch_with_metadata() { async fn test_valid_user_instance() { let service = setup_rg_type_system(); let user = json!({ - "id": "gts.cf.core.idp.user.v1~z.core._.idp_user1.v1", + "id": gts_id!("cf.core.idp.user.v1~z.core._.idp_user1.v1"), "email": "alice@example.com", "display_name": "Alice" }); @@ -497,7 +501,7 @@ async fn test_valid_user_instance() { async fn test_valid_course_instance() { let service = setup_rg_type_system(); let course = json!({ - "id": "gts.cf.core.lms.course.v1~z.core._.lms_course1.v1", + "id": gts_id!("cf.core.lms.course.v1~z.core._.lms_course1.v1"), "title": "Introduction to GTS" }); let results = service.register(vec![course]); @@ -512,7 +516,7 @@ async fn test_valid_course_instance() { async fn test_tenant_missing_required_name() { let service = setup_rg_type_system(); let bad = json!({ - "id": "gts.cf.core.rg.type.v1~y.core.tn.tenant.v1~cf.core._.bad1.v1", + "id": gts_id!("cf.core.rg.type.v1~y.core.tn.tenant.v1~cf.core._.bad1.v1"), "parent_id": null, "tenant_id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "depth": 0 @@ -524,7 +528,7 @@ async fn test_tenant_missing_required_name() { async fn test_tenant_name_too_long() { let service = setup_rg_type_system(); let bad = json!({ - "id": "gts.cf.core.rg.type.v1~y.core.tn.tenant.v1~cf.core._.bad2.v1", + "id": gts_id!("cf.core.rg.type.v1~y.core.tn.tenant.v1~cf.core._.bad2.v1"), "name": "X".repeat(256), "parent_id": null, "tenant_id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", @@ -537,7 +541,7 @@ async fn test_tenant_name_too_long() { async fn test_tenant_empty_name() { let service = setup_rg_type_system(); let bad = json!({ - "id": "gts.cf.core.rg.type.v1~y.core.tn.tenant.v1~cf.core._.bad3.v1", + "id": gts_id!("cf.core.rg.type.v1~y.core.tn.tenant.v1~cf.core._.bad3.v1"), "name": "", "parent_id": null, "tenant_id": "cccccccc-cccc-cccc-cccc-cccccccccccc", @@ -550,7 +554,7 @@ async fn test_tenant_empty_name() { async fn test_user_missing_required_email() { let service = setup_rg_type_system(); let bad = json!({ - "id": "gts.cf.core.idp.user.v1~z.core._.bad_user1.v1", + "id": gts_id!("cf.core.idp.user.v1~z.core._.bad_user1.v1"), "display_name": "Bob" }); assert!(service.register(vec![bad])[0].is_err(), "Missing email"); @@ -560,7 +564,7 @@ async fn test_user_missing_required_email() { async fn test_course_missing_required_title() { let service = setup_rg_type_system(); let bad = json!({ - "id": "gts.cf.core.lms.course.v1~z.core._.bad_course.v1" + "id": gts_id!("cf.core.lms.course.v1~z.core._.bad_course.v1") }); assert!(service.register(vec![bad])[0].is_err(), "Missing title"); } @@ -573,7 +577,7 @@ async fn test_course_missing_required_title() { async fn test_metadata_department_category_too_long() { let service = setup_rg_type_system(); let bad = json!({ - "id": "gts.cf.core.rg.type.v1~w.core.org.department.v1~cf.core._.bad_cat.v1", + "id": gts_id!("cf.core.rg.type.v1~w.core.org.department.v1~cf.core._.bad_cat.v1"), "name": "Bad Dept", "parent_id": "11111111-1111-1111-1111-111111111111", "tenant_id": "11111111-1111-1111-1111-111111111111", @@ -590,7 +594,7 @@ async fn test_metadata_department_category_too_long() { async fn test_metadata_department_short_description_too_long() { let service = setup_rg_type_system(); let bad = json!({ - "id": "gts.cf.core.rg.type.v1~w.core.org.department.v1~cf.core._.bad_desc.v1", + "id": gts_id!("cf.core.rg.type.v1~w.core.org.department.v1~cf.core._.bad_desc.v1"), "name": "Bad Dept", "parent_id": "11111111-1111-1111-1111-111111111111", "tenant_id": "11111111-1111-1111-1111-111111111111", @@ -607,7 +611,7 @@ async fn test_metadata_department_short_description_too_long() { async fn test_metadata_tenant_barrier_wrong_type() { let service = setup_rg_type_system(); let bad = json!({ - "id": "gts.cf.core.rg.type.v1~y.core.tn.tenant.v1~cf.core._.bad_barrier.v1", + "id": gts_id!("cf.core.rg.type.v1~y.core.tn.tenant.v1~cf.core._.bad_barrier.v1"), "name": "Bad Barrier", "parent_id": null, "tenant_id": "dddddddd-dddd-dddd-dddd-dddddddddddd", @@ -624,7 +628,7 @@ async fn test_metadata_tenant_barrier_wrong_type() { async fn test_metadata_tenant_unknown_field_rejected() { let service = setup_rg_type_system(); let bad = json!({ - "id": "gts.cf.core.rg.type.v1~y.core.tn.tenant.v1~cf.core._.bad_unknown.v1", + "id": gts_id!("cf.core.rg.type.v1~y.core.tn.tenant.v1~cf.core._.bad_unknown.v1"), "name": "Unknown Field Tenant", "parent_id": null, "tenant_id": "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee", @@ -645,7 +649,7 @@ async fn test_top_level_custom_field_passes_gts_but_app_layer_rejects() { // not at GTS level. GTS only validates metadata sub-object. let service = setup_rg_type_system(); let instance = json!({ - "id": "gts.cf.core.rg.type.v1~y.core.tn.tenant.v1~cf.core._.flat_ok.v1", + "id": gts_id!("cf.core.rg.type.v1~y.core.tn.tenant.v1~cf.core._.flat_ok.v1"), "name": "Flat Field Tenant", "parent_id": null, "tenant_id": "ffffffff-ffff-ffff-ffff-ffffffffffff", @@ -673,14 +677,14 @@ async fn test_invalid_gts_id_no_prefix() { #[tokio::test] async fn test_invalid_gts_id_uppercase() { let service = setup_rg_type_system(); - let bad = json!({ "$id": "gts://gts.cf.core.Rg.Type.V1~", "$schema": "http://json-schema.org/draft-07/schema#", "type": "object" }); + let bad = json!({ "$id": format!("{GTS_ID_URI_PREFIX}{GTS_ID_PREFIX}cf.core.Rg.Type.V1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object" }); assert!(service.register(vec![bad])[0].is_err()); } #[tokio::test] async fn test_invalid_gts_id_missing_version() { let service = setup_rg_type_system(); - let bad = json!({ "$id": "gts://gts.cf.core.rg.type~", "$schema": "http://json-schema.org/draft-07/schema#", "type": "object" }); + let bad = json!({ "$id": format!("{GTS_ID_URI_PREFIX}{GTS_ID_PREFIX}cf.core.rg.type~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object" }); assert!(service.register(vec![bad])[0].is_err()); } @@ -692,11 +696,11 @@ async fn test_invalid_gts_id_missing_version() { async fn test_chained_type_with_broken_ref_fails_on_ready() { let service = create_service(); let orphan = json!({ - "$id": "gts://gts.cf.core.rg.type.v1~q.nonexistent._.foo.v1~", + "$id": gts_uri!("cf.core.rg.type.v1~q.nonexistent._.foo.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "x-gts-traits": { "can_be_root": true, "allowed_parent_types": [], "allowed_membership_types": [] }, "allOf": [ - { "$ref": "gts://gts.cf.core.rg.type.v1~" } + { "$ref": gts_uri!("cf.core.rg.type.v1~") } ] }); assert!( @@ -719,36 +723,36 @@ async fn test_full_hierarchy_batch() { let instances = vec![ json!({ - "id": "gts.cf.core.rg.type.v1~y.core.tn.tenant.v1~cf.core._.t1.v1", + "id": gts_id!("cf.core.rg.type.v1~y.core.tn.tenant.v1~cf.core._.t1.v1"), "name": "T1", "parent_id": null, "tenant_id": "11111111-1111-1111-1111-111111111111", "depth": 0 }), json!({ - "id": "gts.cf.core.rg.type.v1~w.core.org.department.v1~cf.core._.d2.v1", + "id": gts_id!("cf.core.rg.type.v1~w.core.org.department.v1~cf.core._.d2.v1"), "name": "D2", "parent_id": "11111111-1111-1111-1111-111111111111", "tenant_id": "11111111-1111-1111-1111-111111111111", "depth": 1, "metadata": { "category": "finance", "short_description": "Mega Department" } }), json!({ - "id": "gts.cf.core.rg.type.v1~cf.core.rg.branch.v1~cf.core._.b3.v1", + "id": gts_id!("cf.core.rg.type.v1~cf.core.rg.branch.v1~cf.core._.b3.v1"), "name": "B3", "parent_id": "22222222-2222-2222-2222-222222222222", "tenant_id": "11111111-1111-1111-1111-111111111111", "depth": 2, "metadata": { "location": "Building A, Floor 3" } }), json!({ - "id": "gts.cf.core.rg.type.v1~y.core.tn.tenant.v1~cf.core._.t7.v1", + "id": gts_id!("cf.core.rg.type.v1~y.core.tn.tenant.v1~cf.core._.t7.v1"), "name": "T7", "parent_id": "11111111-1111-1111-1111-111111111111", "tenant_id": "77777777-7777-7777-7777-777777777777", "depth": 1, "metadata": { "self_managed": true } }), json!({ - "id": "gts.cf.core.rg.type.v1~w.core.org.department.v1~cf.core._.d8.v1", + "id": gts_id!("cf.core.rg.type.v1~w.core.org.department.v1~cf.core._.d8.v1"), "name": "D8", "parent_id": "77777777-7777-7777-7777-777777777777", "tenant_id": "77777777-7777-7777-7777-777777777777", "depth": 2, "metadata": { "category": "hr" } }), json!({ - "id": "gts.cf.core.rg.type.v1~y.core.tn.tenant.v1~cf.core._.t9.v1", + "id": gts_id!("cf.core.rg.type.v1~y.core.tn.tenant.v1~cf.core._.t9.v1"), "name": "T9", "parent_id": null, "tenant_id": "99999999-9999-9999-9999-999999999999", "depth": 0, "metadata": { "custom_domain": "t9.example.com" } @@ -775,6 +779,7 @@ async fn test_full_hierarchy_batch() { #[tokio::test] async fn test_config_mode_accepts_invalid_then_ready_rejects() { + const TENANT_TYPE_ID: &str = gts_id!("cf.core.rg.type.v1~y.core.tn.tenant.v1~"); let service = create_service(); let schemas = vec![ @@ -789,7 +794,7 @@ async fn test_config_mode_accepts_invalid_then_ready_rejects() { // Invalid tenant: missing name (accepted in config mode) let bad = json!({ - "id": "gts.cf.core.rg.type.v1~y.core.tn.tenant.v1~cf.core._.bad_cfg.v1", + "id": gts_id!("cf.core.rg.type.v1~y.core.tn.tenant.v1~cf.core._.bad_cfg.v1"), "parent_id": null, "tenant_id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "depth": 0 }); assert!( @@ -802,10 +807,7 @@ async fn test_config_mode_accepts_invalid_then_ready_rejects() { let err = ready_result.unwrap_err(); let errors = err.validation_errors().unwrap(); assert!(!errors.is_empty()); - assert!(errors.iter().any(|e| { - e.gts_id - .contains("gts.cf.core.rg.type.v1~y.core.tn.tenant.v1~") - })); + assert!(errors.iter().any(|e| { e.gts_id.contains(TENANT_TYPE_ID) })); } // ============================================================================= @@ -816,7 +818,7 @@ async fn test_config_mode_accepts_invalid_then_ready_rejects() { async fn test_wildcard_query_all_rg_chained_types() { let service = setup_rg_type_system(); let results = service - .list(&ListQuery::default().with_pattern("gts.cf.core.rg.type.v1~*")) + .list(&ListQuery::default().with_pattern(format!("{GTS_ID_PREFIX}cf.core.rg.type.v1~*"))) .unwrap(); assert!(results.len() >= 3); } @@ -829,7 +831,7 @@ async fn test_query_by_x_core_rg_prefix() { // rg branch type, and every chain derived from `gts.cf.core.rg.type.v1~`. let service = setup_rg_type_system(); let results = service - .list(&ListQuery::default().with_pattern("gts.cf.core.rg.*")) + .list(&ListQuery::default().with_pattern(format!("{GTS_ID_PREFIX}cf.core.rg.*"))) .unwrap(); assert!(!results.is_empty()); } @@ -848,7 +850,7 @@ async fn test_idempotent_schema_registration() { async fn test_modified_schema_reregistration_fails() { let service = setup_rg_type_system(); let modified = json!({ - "$id": "gts://gts.cf.core.tn.tenant.v1~", + "$id": gts_uri!("cf.core.tn.tenant.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "title": "Tenant MODIFIED", "type": "object", "required": ["id", "name"], diff --git a/gears/system/types-registry/types-registry/tests/type_instance_tests.rs b/gears/system/types-registry/types-registry/tests/type_instance_tests.rs index 7ba2519be..507dd6732 100644 --- a/gears/system/types-registry/types-registry/tests/type_instance_tests.rs +++ b/gears/system/types-registry/types-registry/tests/type_instance_tests.rs @@ -6,6 +6,8 @@ mod common; use common::create_service; use serde_json::json; +use toolkit_gts::gts_id; +use toolkit_gts::gts_uri; use types_registry::domain::model::ListQuery; // ============================================================================= @@ -19,7 +21,7 @@ async fn test_type_with_valid_instances() { // Register a base type schema (User type) // Types end with ~ and define the schema let user_type = json!({ - "$id": "gts://gts.acme.core.models.user.v1~", + "$id": gts_uri!("acme.core.models.user.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { @@ -46,7 +48,7 @@ async fn test_type_with_valid_instances() { // Instances have format: parent~instance (at least 2 segments) // Note: "type" field is not needed - schema ID is derived from $id let valid_instance1 = json!({ - "id": "gts.acme.core.models.user.v1~acme.core.instances.user1.v1", + "id": gts_id!("acme.core.models.user.v1~acme.core.instances.user1.v1"), "userId": "user-001", "email": "alice@example.com", "age": 30, @@ -54,7 +56,7 @@ async fn test_type_with_valid_instances() { }); let valid_instance2 = json!({ - "id": "gts.acme.core.models.user.v1~acme.core.instances.user2.v1", + "id": gts_id!("acme.core.models.user.v1~acme.core.instances.user2.v1"), "userId": "user-002", "email": "bob@example.com" // age and isActive are optional @@ -75,11 +77,15 @@ async fn test_type_with_valid_instances() { ); // Verify instances are retrievable - let i1 = service.get("gts.acme.core.models.user.v1~acme.core.instances.user1.v1"); + let i1 = service.get(gts_id!( + "acme.core.models.user.v1~acme.core.instances.user1.v1" + )); assert!(i1.is_ok()); assert!(i1.unwrap().is_instance()); - let i2 = service.get("gts.acme.core.models.user.v1~acme.core.instances.user2.v1"); + let i2 = service.get(gts_id!( + "acme.core.models.user.v1~acme.core.instances.user2.v1" + )); assert!(i2.is_ok()); } @@ -89,7 +95,7 @@ async fn test_type_with_invalid_instance_missing_required_field() { // Register a type with required fields let order_type = json!({ - "$id": "gts://gts.acme.core.models.order.v1~", + "$id": gts_uri!("acme.core.models.order.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { @@ -107,7 +113,7 @@ async fn test_type_with_invalid_instance_missing_required_field() { // Try to register an instance missing required "total" field // Note: "type" field is not needed - schema ID is derived from $id let invalid_instance = json!({ - "id": "gts.acme.core.models.order.v1~acme.core.instances.order1.v1", + "id": gts_id!("acme.core.models.order.v1~acme.core.instances.order1.v1"), "orderId": "order-001", "customerId": "cust-001" // Missing required "total" field @@ -129,7 +135,7 @@ async fn test_type_with_invalid_instance_wrong_field_type() { // Register a type with specific field types let product_type = json!({ - "$id": "gts://gts.acme.core.models.product.v1~", + "$id": gts_uri!("acme.core.models.product.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { @@ -148,7 +154,7 @@ async fn test_type_with_invalid_instance_wrong_field_type() { // Try to register an instance with wrong type for "price" (string instead of number) // Note: "type" field is not needed - schema ID is derived from $id let invalid_instance = json!({ - "id": "gts.acme.core.models.product.v1~acme.core.instances.prod1.v1", + "id": gts_id!("acme.core.models.product.v1~acme.core.instances.prod1.v1"), "productId": "prod-001", "name": "Widget", "price": "not-a-number", // Should be a number @@ -171,7 +177,7 @@ async fn test_multiple_instances_of_same_type() { // Register an event type let event_type = json!({ - "$id": "gts://gts.acme.core.events.user_action.v1~", + "$id": gts_uri!("acme.core.events.user_action.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { @@ -191,21 +197,21 @@ async fn test_multiple_instances_of_same_type() { // Note: "type" field is not needed - schema ID is derived from $id let instances = vec![ json!({ - "id": "gts.acme.core.events.user_action.v1~acme.core.instances.event1.v1", + "id": gts_id!("acme.core.events.user_action.v1~acme.core.instances.event1.v1"), "eventId": "evt-001", "userId": "user-001", "action": "login", "timestamp": "2024-01-15T10:30:00Z" }), json!({ - "id": "gts.acme.core.events.user_action.v1~acme.core.instances.event2.v1", + "id": gts_id!("acme.core.events.user_action.v1~acme.core.instances.event2.v1"), "eventId": "evt-002", "userId": "user-001", "action": "purchase", "timestamp": "2024-01-15T11:00:00Z" }), json!({ - "id": "gts.acme.core.events.user_action.v1~acme.core.instances.event3.v1", + "id": gts_id!("acme.core.events.user_action.v1~acme.core.instances.event3.v1"), "eventId": "evt-003", "userId": "user-002", "action": "logout", @@ -243,7 +249,7 @@ async fn test_nested_object_type_with_instance() { // Register a complex type with nested objects let customer_type = json!({ - "$id": "gts://gts.acme.core.models.customer.v1~", + "$id": gts_uri!("acme.core.models.customer.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { @@ -269,7 +275,7 @@ async fn test_nested_object_type_with_instance() { // Register a valid customer instance with nested address // Note: "type" field is not needed - schema ID is derived from $id let valid_customer = json!({ - "id": "gts.acme.core.models.customer.v1~acme.core.instances.cust1.v1", + "id": gts_id!("acme.core.models.customer.v1~acme.core.instances.cust1.v1"), "customerId": "cust-001", "name": "Acme Corp", "billingAddress": { @@ -287,7 +293,9 @@ async fn test_nested_object_type_with_instance() { ); // Verify the instance - let customer = service.get("gts.acme.core.models.customer.v1~acme.core.instances.cust1.v1"); + let customer = service.get(gts_id!( + "acme.core.models.customer.v1~acme.core.instances.cust1.v1" + )); assert!(customer.is_ok()); assert!(customer.unwrap().is_instance()); } @@ -298,7 +306,7 @@ async fn test_array_type_with_instance() { // Register a type with array properties let cart_type = json!({ - "$id": "gts://gts.acme.core.models.cart.v1~", + "$id": gts_uri!("acme.core.models.cart.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { @@ -325,7 +333,7 @@ async fn test_array_type_with_instance() { // Register a valid cart instance with array items // Note: "type" field is not needed - schema ID is derived from $id let valid_cart = json!({ - "id": "gts.acme.core.models.cart.v1~acme.core.instances.cart1.v1", + "id": gts_id!("acme.core.models.cart.v1~acme.core.instances.cart1.v1"), "cartId": "cart-001", "items": [ { "productId": "prod-001", "quantity": 2 }, @@ -347,7 +355,7 @@ async fn test_instance_with_mismatched_type_field_is_ignored_for_well_known_inst // Register two different type schemas let user_type = json!({ - "$id": "gts://gts.acme.core.models.user.v1~", + "$id": gts_uri!("acme.core.models.user.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { @@ -359,7 +367,7 @@ async fn test_instance_with_mismatched_type_field_is_ignored_for_well_known_inst }); let product_type = json!({ - "$id": "gts://gts.acme.core.models.product.v1~", + "$id": gts_uri!("acme.core.models.product.v1~"), "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { @@ -378,8 +386,8 @@ async fn test_instance_with_mismatched_type_field_is_ignored_for_well_known_inst // - "type" field explicitly claims parent is "product" type (gts.acme.core.models.product.v1~) // Chained GTS ID ALWAYS takes priority over explicit type field, so validation uses user schema let mismatched_instance = json!({ - "id": "gts.acme.core.models.user.v1~acme.core.instances.user1.v1", - "type": "gts.acme.core.models.product.v1~", + "id": gts_id!("acme.core.models.user.v1~acme.core.instances.user1.v1"), + "type": gts_id!("acme.core.models.product.v1~"), "userId": "user-001", "name": "Alice" }); diff --git a/gears/system/usage-collector/usage-collector-sdk/src/gts.rs b/gears/system/usage-collector/usage-collector-sdk/src/gts.rs index 3c44274b6..1d08fce54 100644 --- a/gears/system/usage-collector/usage-collector-sdk/src/gts.rs +++ b/gears/system/usage-collector/usage-collector-sdk/src/gts.rs @@ -1,18 +1,19 @@ //! GTS plugin spec for usage-collector storage-plugin discovery and binding. use toolkit::gts::PluginV1; +use toolkit_gts::gts_id; use toolkit_gts::gts_type_schema; /// Canonical GTS resource type for Usage Type /// `resource_type` carried by [`crate::UsageCollectorError`] envelopes about /// a usage type (`create` / `get` / `list` / `delete`). Match /// [`crate::UsageCollectorError::NotFound::resource_type`] etc. against this. -pub const USAGE_TYPE_RESOURCE: &str = "gts.cf.core.uc.usage_type.v1~"; +pub const USAGE_TYPE_RESOURCE: &str = gts_id!("cf.core.uc.usage_type.v1~"); /// Canonical GTS resource type for the **ingestion** surface — the wire /// `resource_type` carried by [`crate::UsageCollectorError`] envelopes about /// a usage record (`create` / `deactivate` / `list` / `aggregate`). -pub const USAGE_RECORD_RESOURCE: &str = "gts.cf.core.uc.usage_record.v1~"; +pub const USAGE_RECORD_RESOURCE: &str = gts_id!("cf.core.uc.usage_record.v1~"); /// GTS plugin specification for usage-collector storage backends. // @cpt-dod:cpt-cf-usage-collector-dod-foundation-contract-storage-plugin:p1 @@ -21,7 +22,7 @@ pub const USAGE_RECORD_RESOURCE: &str = "gts.cf.core.uc.usage_record.v1~"; #[gts_type_schema( dir_path = "schemas", base = PluginV1, - type_id = "gts.cf.toolkit.plugins.plugin.v1~cf.core.uc.plugin.v1~", + type_id = gts_id!("cf.toolkit.plugins.plugin.v1~cf.core.uc.plugin.v1~"), description = "Usage Collector plugin specification", properties = "", )] diff --git a/gears/system/usage-collector/usage-collector-sdk/src/models.rs b/gears/system/usage-collector/usage-collector-sdk/src/models.rs index 161171ceb..e63075df5 100644 --- a/gears/system/usage-collector/usage-collector-sdk/src/models.rs +++ b/gears/system/usage-collector/usage-collector-sdk/src/models.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; use toolkit_odata_macros::ODataFilterable; use uuid::Uuid; -use gts::{GtsID, GtsInstanceId}; +use gts::{GtsId, GtsIdSegment, GtsInstanceId}; use crate::error::UsageCollectorError; @@ -450,12 +450,12 @@ impl UsageTypeGtsId { /// Every catalog `gts_id` MUST left-prefix-match this value and carry at /// least one further `~`-separated derivation segment. The base itself /// is abstract and rejected as a bare value. - pub const USAGE_RECORD_BASE: &'static str = "gts.cf.core.uc.usage_record.v1~"; + pub const USAGE_RECORD_BASE: &'static str = crate::gts::USAGE_RECORD_RESOURCE; /// Creates a `UsageTypeGtsId` after validating that the input is a /// well-formed GTS instance id deriving from [`Self::USAGE_RECORD_BASE`]. /// - /// Validation routes through [`gts::GtsID::new`], which enforces the GTS + /// Validation routes through [`gts::GtsId::try_new`], which enforces the GTS /// per-segment grammar (`vendor.package.namespace.type.v[.]`), /// the allowed character set, terminator semantics, and the chained-id /// rules. That is the same validator other gears use for catalog-key @@ -472,7 +472,7 @@ impl UsageTypeGtsId { /// segment of the parsed chain). pub fn new(value: impl Into) -> Result { let raw = value.into(); - let parsed = GtsID::new(&raw).map_err(|e| { + let parsed = GtsId::try_new(&raw).map_err(|e| { UsageCollectorError::invalid_usage_type_gts_id( &raw, &format!("usage type gts_id `{raw}` is not a valid GTS id: {e}"), @@ -508,7 +508,7 @@ impl UsageTypeGtsId { // returning `Some(base)` implies `gts_id_segments.len() >= 2` — // but is kept as a graceful error rather than `expect` to satisfy // the workspace `clippy::expect_used` rule. - let Some(segment) = parsed.gts_id_segments.last().map(|s| s.segment.as_str()) else { + let Some(segment) = parsed.segments().last().map(GtsIdSegment::raw) else { return Err(UsageCollectorError::invalid_usage_type_gts_id( &raw, &format!("usage type gts_id `{raw}` is missing a derivation segment"), diff --git a/gears/system/usage-collector/usage-collector-sdk/src/models_tests.rs b/gears/system/usage-collector/usage-collector-sdk/src/models_tests.rs index 56af775c0..3d2e1ddfb 100644 --- a/gears/system/usage-collector/usage-collector-sdk/src/models_tests.rs +++ b/gears/system/usage-collector/usage-collector-sdk/src/models_tests.rs @@ -4,6 +4,7 @@ use std::str::FromStr; use rust_decimal::Decimal; use serde_json::json; +use toolkit_gts::{GTS_ID_PREFIX, gts_id}; use uuid::Uuid; use std::collections::{BTreeMap, BTreeSet}; @@ -32,7 +33,7 @@ fn metadata_map(entries: [(&str, &str); N]) -> BTreeMap UsageTypeGtsId { UsageTypeGtsId::new(SAMPLE_USAGE_TYPE_ID).expect("valid usage_record-derived id") @@ -71,7 +72,7 @@ fn sample_usage_record(subject_ref: Option, corrects_id: Option(json!("gts.cf.core.metric.v1~oops")) - .expect_err("malformed gts_id must surface as a serde error"); + let err = serde_json::from_value::(json!(format!( + "{GTS_ID_PREFIX}cf.core.metric.v1~oops" + ))) + .expect_err("malformed gts_id must surface as a serde error"); assert!( err.to_string().contains("usage type gts_id"), "serde error must carry the Validation detail; got {err}" @@ -323,7 +334,7 @@ fn usage_type_serde_round_trip_carries_kind_and_metadata_fields() { #[test] fn usage_type_rejects_unknown_fields() { let payload = json!({ - "gts_id": "gts.cf.core.uc.usage_record.v1~cf.compute._.vcpu_hours.v1", + "gts_id": gts_id!("cf.core.uc.usage_record.v1~cf.compute._.vcpu_hours.v1"), "kind": "counter", "metadata_fields": [], "legacy_schema_field": {"type": "object"}, @@ -336,7 +347,7 @@ fn usage_type_rejects_unknown_fields() { #[test] fn usage_type_rejects_unknown_kind_at_deserialize_boundary() { let payload = json!({ - "gts_id": "gts.cf.core.uc.usage_record.v1~cf.compute._.vcpu_hours.v1", + "gts_id": gts_id!("cf.core.uc.usage_record.v1~cf.compute._.vcpu_hours.v1"), "kind": "histogram", "metadata_fields": [], }); diff --git a/gears/system/usage-collector/usage-collector/Cargo.toml b/gears/system/usage-collector/usage-collector/Cargo.toml index 1ca04ee60..ae5f198be 100644 --- a/gears/system/usage-collector/usage-collector/Cargo.toml +++ b/gears/system/usage-collector/usage-collector/Cargo.toml @@ -55,3 +55,4 @@ inventory = { workspace = true } [dev-dependencies] types-registry-sdk = { workspace = true, features = ["test-util"] } tokio-util = { workspace = true } +toolkit-gts = { workspace = true } diff --git a/gears/system/usage-collector/usage-collector/src/api/rest/dto_tests.rs b/gears/system/usage-collector/usage-collector/src/api/rest/dto_tests.rs index c41a7845b..827a2fc74 100644 --- a/gears/system/usage-collector/usage-collector/src/api/rest/dto_tests.rs +++ b/gears/system/usage-collector/usage-collector/src/api/rest/dto_tests.rs @@ -8,10 +8,12 @@ use std::collections::BTreeMap; use std::str::FromStr; +use toolkit_gts::gts_uri; use rust_decimal::Decimal; use time::OffsetDateTime; use toolkit_canonical_errors::Problem; +use toolkit_gts::gts_id; use usage_collector_sdk::{ IdempotencyKey, ResourceRef, UsageCollectorError, UsageRecord, UsageRecordStatus, UsageTypeGtsId, ValidationReason, @@ -23,7 +25,7 @@ use super::{ }; const SAMPLE_USAGE_TYPE_ID: &str = - "gts.cf.core.uc.usage_record.v1~cf.mini_chat._.tokens_consumed.v1"; + gts_id!("cf.core.uc.usage_record.v1~cf.mini_chat._.tokens_consumed.v1"); const SAMPLE_RECORD_RFC3339: &str = "2026-06-11T12:34:56Z"; const SAMPLE_RECORD_VALUE: &str = "42.5"; @@ -372,7 +374,7 @@ fn create_usage_record_result_dto_serialises_accepted_with_lowercase_tag() { #[test] fn create_usage_record_result_dto_serialises_rejected_with_lowercase_tag() { let problem = Problem { - problem_type: "gts://gts.cf.core.errors.err.v1~cf.core.err.invalid_argument.v1~".to_owned(), + problem_type: gts_uri!("cf.core.errors.err.v1~cf.core.err.invalid_argument.v1~").to_owned(), title: "Invalid argument".to_owned(), status: 400, detail: "test rejection".to_owned(), diff --git a/gears/system/usage-collector/usage-collector/src/api/rest/handlers/usage_records_tests.rs b/gears/system/usage-collector/usage-collector/src/api/rest/handlers/usage_records_tests.rs index 59720cd64..315bad3b2 100644 --- a/gears/system/usage-collector/usage-collector/src/api/rest/handlers/usage_records_tests.rs +++ b/gears/system/usage-collector/usage-collector/src/api/rest/handlers/usage_records_tests.rs @@ -17,6 +17,7 @@ //! [`crate::domain::service::service_tests`]. use std::sync::Arc; +use toolkit_gts::gts_id; use axum::Json; use axum::extract::{Extension, Path}; @@ -336,7 +337,7 @@ use usage_collector_sdk::{ }; const HAPPY_RECORD_GTS_ID: &str = - "gts.cf.core.uc.usage_record.v1~cf.mini_chat._.tokens_consumed.v1"; + gts_id!("cf.core.uc.usage_record.v1~cf.mini_chat._.tokens_consumed.v1"); fn happy_usage_type() -> UsageType { UsageType { @@ -1300,6 +1301,7 @@ mod parse_required_gts_id_tests { use toolkit_canonical_errors::context::InvalidArgumentV1; use super::super::parse_required_gts_id; + use super::HAPPY_RECORD_GTS_ID; fn p(key: &str, value: &str) -> (String, String) { (key.to_owned(), value.to_owned()) @@ -1341,8 +1343,7 @@ mod parse_required_gts_id_tests { // would silently mask the caller bug. The helper MUST reject // outright. Pin BOTH the field/reason and that the rejection // happens regardless of whether either value is well-formed. - let valid_gts = - "gts.cf.core.uc.usage_record.v1~cf.mini_chat._.tokens_consumed.v1".to_owned(); + let valid_gts = HAPPY_RECORD_GTS_ID.to_owned(); let err = parse_required_gts_id(&[p("gts_id", &valid_gts), p("gts_id", "even.something.else")]) .expect_err("duplicate gts_id MUST reject"); @@ -1371,7 +1372,7 @@ mod parse_required_gts_id_tests { #[test] fn well_formed_gts_id_round_trips_through_the_typed_newtype() { - let raw = "gts.cf.core.uc.usage_record.v1~cf.mini_chat._.tokens_consumed.v1"; + let raw = HAPPY_RECORD_GTS_ID; let parsed = parse_required_gts_id(&[p("gts_id", raw)]).expect("well-formed gts_id passes"); assert_eq!(AsRef::::as_ref(&parsed), raw); } @@ -1583,7 +1584,7 @@ mod handle_list_usage_records_tests { use uuid::Uuid; use super::super::handle_list_usage_records; - use super::sample_persisted_record; + use super::{HAPPY_RECORD_GTS_ID, sample_persisted_record}; use crate::domain::Service; use crate::domain::test_support::{ CountingPermitResolver, CountingUnreachableResolver, HappyPathPlugin, authenticated_ctx, @@ -1659,10 +1660,7 @@ mod handle_list_usage_records_tests { Extension(SecurityContext::anonymous()), Extension(service), Query(vec![ - ( - "gts_id".to_owned(), - "gts.cf.core.uc.usage_record.v1~cf.mini_chat._.tokens_consumed.v1".to_owned(), - ), + ("gts_id".to_owned(), HAPPY_RECORD_GTS_ID.to_owned()), ("totally_unknown".to_owned(), "x".to_owned()), ]), OData(ODataQuery::new()), @@ -1693,10 +1691,7 @@ mod handle_list_usage_records_tests { let response = handle_list_usage_records( Extension(SecurityContext::anonymous()), Extension(service), - Query(vec![( - "gts_id".to_owned(), - "gts.cf.core.uc.usage_record.v1~cf.mini_chat._.tokens_consumed.v1".to_owned(), - )]), + Query(vec![("gts_id".to_owned(), HAPPY_RECORD_GTS_ID.to_owned())]), OData(q), ) .await @@ -1732,10 +1727,7 @@ mod handle_list_usage_records_tests { let response = handle_list_usage_records( Extension(authenticated_ctx()), Extension(service), - Query(vec![( - "gts_id".to_owned(), - "gts.cf.core.uc.usage_record.v1~cf.mini_chat._.tokens_consumed.v1".to_owned(), - )]), + Query(vec![("gts_id".to_owned(), HAPPY_RECORD_GTS_ID.to_owned())]), OData(super::bounded_window_query()), ) .await @@ -1813,6 +1805,7 @@ mod handle_list_usage_records_tests { mod handle_query_aggregated_usage_records_tests { use std::sync::Arc; + use toolkit_gts::gts_id; use axum::Json; use axum::extract::{Extension, Query}; @@ -1836,7 +1829,8 @@ mod handle_query_aggregated_usage_records_tests { enforcer_for, hub_with_plugin, }; - const VALID_GTS_ID: &str = "gts.cf.core.uc.usage_record.v1~cf.mini_chat._.tokens_consumed.v1"; + const VALID_GTS_ID: &str = + gts_id!("cf.core.uc.usage_record.v1~cf.mini_chat._.tokens_consumed.v1"); fn service_no_plugin() -> Arc { let hub = Arc::new(ClientHub::new()); diff --git a/gears/system/usage-collector/usage-collector/src/api/rest/handlers/usage_types_tests.rs b/gears/system/usage-collector/usage-collector/src/api/rest/handlers/usage_types_tests.rs index 7d4514544..d627fac5f 100644 --- a/gears/system/usage-collector/usage-collector/src/api/rest/handlers/usage_types_tests.rs +++ b/gears/system/usage-collector/usage-collector/src/api/rest/handlers/usage_types_tests.rs @@ -28,6 +28,7 @@ use axum::Json; use axum::extract::Extension; use axum::http::{StatusCode, Uri, header}; use toolkit::client_hub::ClientHub; +use toolkit_gts::gts_id; use toolkit_security::SecurityContext; use super::{ @@ -297,7 +298,8 @@ async fn register_with_unknown_kind_returns_validation_problem() { .parse() .expect("static uri parses"); - let raw_gts_id = "gts.cf.core.uc.usage_record.v1~tenant.example._.unknown_kind.v1".to_owned(); + let raw_gts_id = + gts_id!("cf.core.uc.usage_record.v1~tenant.example._.unknown_kind.v1").to_owned(); let response = handle_create_usage_type( Extension(SecurityContext::anonymous()), Extension(service), @@ -375,7 +377,7 @@ use toolkit_odata::{ODataQuery, Page as ODataPage, page::PageInfo}; use usage_collector_sdk::{UsageKind, UsageType, UsageTypeGtsId}; const HAPPY_USAGE_TYPE_GTS_ID: &str = - "gts.cf.core.uc.usage_record.v1~cf.handler_tests._.happy_counter.v1"; + gts_id!("cf.core.uc.usage_record.v1~cf.handler_tests._.happy_counter.v1"); fn happy_usage_type() -> UsageType { UsageType { diff --git a/gears/system/usage-collector/usage-collector/src/domain/authz.rs b/gears/system/usage-collector/usage-collector/src/domain/authz.rs index ebb334374..95c603338 100644 --- a/gears/system/usage-collector/usage-collector/src/domain/authz.rs +++ b/gears/system/usage-collector/usage-collector/src/domain/authz.rs @@ -103,10 +103,10 @@ impl AttributionTupleKey { /// alone and the [`RESOURCE`] declares no attributes. pub(crate) mod usage_type { use authz_resolver_sdk::pep::ResourceType; + use usage_collector_sdk::USAGE_TYPE_RESOURCE; /// PEP resource type for the `UsageType` catalog. - pub const RESOURCE: ResourceType = - ResourceType::from_static("gts.cf.core.uc.usage_type.v1~", &[]); + pub const RESOURCE: ResourceType = ResourceType::from_static(USAGE_TYPE_RESOURCE, &[]); /// `UsageType` action vocabulary. Renaming any of these is a contract /// change against the PDP policy bundle. @@ -130,6 +130,7 @@ pub(crate) mod usage_type { pub(crate) mod usage_record { use authz_resolver_sdk::pep::ResourceType; use toolkit_security::pep_properties; + use usage_collector_sdk::USAGE_RECORD_RESOURCE; /// PEP attribute key carrying the caller-supplied `resource_type`. pub const PROP_RESOURCE_TYPE: &str = "resource_type"; @@ -145,7 +146,7 @@ pub(crate) mod usage_record { /// PEP resource type for the `UsageRecord` ingestion surface. Declares the /// attribution-tuple attributes the PDP may key its policy on. pub const RESOURCE: ResourceType = ResourceType::from_static( - "gts.cf.core.uc.usage_record.v1~", + USAGE_RECORD_RESOURCE, &[ pep_properties::OWNER_TENANT_ID, pep_properties::OWNER_ID, diff --git a/gears/system/usage-collector/usage-collector/src/domain/authz_tests.rs b/gears/system/usage-collector/usage-collector/src/domain/authz_tests.rs index cefec52ed..8cae14f9f 100644 --- a/gears/system/usage-collector/usage-collector/src/domain/authz_tests.rs +++ b/gears/system/usage-collector/usage-collector/src/domain/authz_tests.rs @@ -17,6 +17,7 @@ use std::collections::BTreeMap; use std::sync::Arc; +use toolkit_gts::gts_id; use authz_resolver_sdk::models::EvaluationRequest; use rust_decimal::Decimal; @@ -32,7 +33,7 @@ use super::{ }; use crate::domain::test_support::{CapturingTenantPermitResolver, enforcer_for}; -const SAMPLE_GTS_ID: &str = "gts.cf.core.uc.usage_record.v1~cf.mini_chat._.tokens_consumed.v1"; +const SAMPLE_GTS_ID: &str = gts_id!("cf.core.uc.usage_record.v1~cf.mini_chat._.tokens_consumed.v1"); fn ctx() -> SecurityContext { SecurityContext::builder() diff --git a/gears/system/usage-collector/usage-collector/src/domain/error_tests.rs b/gears/system/usage-collector/usage-collector/src/domain/error_tests.rs index 6a3ad3495..14318103c 100644 --- a/gears/system/usage-collector/usage-collector/src/domain/error_tests.rs +++ b/gears/system/usage-collector/usage-collector/src/domain/error_tests.rs @@ -7,6 +7,7 @@ //! error-envelope compaction — the public envelope is terminal — so these //! tests exercise the plugin->domain and domain->SDK directions only. +use toolkit_gts::gts_id; use usage_collector_sdk::{ ConflictReason, USAGE_RECORD_RESOURCE, USAGE_TYPE_RESOURCE, UsageCollectorError, UsageCollectorPluginError, UsageTypeGtsId, ValidationReason, @@ -15,7 +16,7 @@ use usage_collector_sdk::{ use super::*; const SAMPLE_USAGE_TYPE_ID: &str = - "gts.cf.core.uc.usage_record.v1~cf.mini_chat._.tokens_consumed.v1"; + gts_id!("cf.core.uc.usage_record.v1~cf.mini_chat._.tokens_consumed.v1"); fn sample_gts_id() -> UsageTypeGtsId { UsageTypeGtsId::new(SAMPLE_USAGE_TYPE_ID).expect("valid usage_record-derived usage-type gts_id") diff --git a/gears/system/usage-collector/usage-collector/src/domain/local_client_tests.rs b/gears/system/usage-collector/usage-collector/src/domain/local_client_tests.rs index 83604ca8f..a52447ebe 100644 --- a/gears/system/usage-collector/usage-collector/src/domain/local_client_tests.rs +++ b/gears/system/usage-collector/usage-collector/src/domain/local_client_tests.rs @@ -9,6 +9,7 @@ use std::sync::Arc; use toolkit::client_hub::ClientHub; +use toolkit_gts::gts_id; use toolkit_security::SecurityContext; use usage_collector_sdk::{ UsageCollectorClientV1, UsageCollectorError, UsageKind, UsageType, UsageTypeGtsId, @@ -20,7 +21,7 @@ use crate::domain::test_support::{UnreachableResolver, enforcer_for}; use super::*; const SAMPLE_USAGE_TYPE_ID: &str = - "gts.cf.core.uc.usage_record.v1~cf.mini_chat._.tokens_consumed.v1"; + gts_id!("cf.core.uc.usage_record.v1~cf.mini_chat._.tokens_consumed.v1"); fn make_client() -> UsageCollectorLocalClient { let hub = Arc::new(ClientHub::new()); diff --git a/gears/system/usage-collector/usage-collector/src/domain/service_tests.rs b/gears/system/usage-collector/usage-collector/src/domain/service_tests.rs index db0d9ad10..6781a6535 100644 --- a/gears/system/usage-collector/usage-collector/src/domain/service_tests.rs +++ b/gears/system/usage-collector/usage-collector/src/domain/service_tests.rs @@ -312,6 +312,7 @@ mod create_usage_type_tests { use std::sync::Arc; use std::sync::Mutex; use std::sync::atomic::{AtomicUsize, Ordering}; + use toolkit_gts::gts_id; use std::collections::BTreeSet; @@ -332,8 +333,8 @@ mod create_usage_type_tests { use crate::domain::local_client::UsageCollectorLocalClient; use crate::domain::test_support::{CountingAllowAllResolver, DenyAllResolver, enforcer_for}; - const COUNTER_ID: &str = "gts.cf.core.uc.usage_record.v1~example.usage._.bytes_in.v1"; - const GAUGE_ID: &str = "gts.cf.core.uc.usage_record.v1~example.usage._.cpu_load.v1"; + const COUNTER_ID: &str = gts_id!("cf.core.uc.usage_record.v1~example.usage._.bytes_in.v1"); + const GAUGE_ID: &str = gts_id!("cf.core.uc.usage_record.v1~example.usage._.cpu_load.v1"); fn keyset(values: [&str; N]) -> BTreeSet { values @@ -826,6 +827,7 @@ mod catalog_dispatch_tests { use std::sync::Arc; use std::sync::Mutex; use std::sync::atomic::{AtomicUsize, Ordering}; + use toolkit_gts::gts_id; use async_trait::async_trait; use toolkit::client_hub::{ClientHub, ClientScope}; @@ -851,7 +853,7 @@ mod catalog_dispatch_tests { .collect() } - const COUNTER_ID: &str = "gts.cf.core.uc.usage_record.v1~example.usage._.bytes_in.v1"; + const COUNTER_ID: &str = gts_id!("cf.core.uc.usage_record.v1~example.usage._.bytes_in.v1"); // ── programmable stub plugin: per-op response queues + call counters ──── @@ -1434,6 +1436,7 @@ mod deactivate_usage_record_tests { use std::sync::Arc; use std::sync::Mutex; use std::sync::atomic::{AtomicUsize, Ordering}; + use toolkit_gts::gts_id; use async_trait::async_trait; use toolkit::client_hub::{ClientHub, ClientScope}; @@ -1521,9 +1524,9 @@ mod deactivate_usage_record_tests { use usage_collector_sdk::{IdempotencyKey, ResourceRef, UsageRecordStatus, UsageTypeGtsId}; UsageRecord { uuid: Uuid::from_u128(0xAAAA_AAAA), - gts_id: UsageTypeGtsId::new( - "gts.cf.core.uc.usage_record.v1~cf.mini_chat._.tokens_consumed.v1", - ) + gts_id: UsageTypeGtsId::new(gts_id!( + "cf.core.uc.usage_record.v1~cf.mini_chat._.tokens_consumed.v1" + )) .expect("valid usage_record-derived gts_id"), tenant_id: Uuid::from_u128(2), resource_ref: ResourceRef::new("rsc-stub", "compute.vm").expect("valid resource ref"), @@ -1964,6 +1967,7 @@ mod pdp_dedup_tests { use std::collections::BTreeMap; use std::collections::BTreeSet; use std::sync::Arc; + use toolkit_gts::gts_id; use time::OffsetDateTime; use usage_collector_sdk::{ @@ -1977,7 +1981,8 @@ mod pdp_dedup_tests { service_with_counting_permit, }; - const HAPPY_GTS_ID: &str = "gts.cf.core.uc.usage_record.v1~cf.mini_chat._.tokens_consumed.v1"; + const HAPPY_GTS_ID: &str = + gts_id!("cf.core.uc.usage_record.v1~cf.mini_chat._.tokens_consumed.v1"); fn happy_usage_type() -> UsageType { UsageType { @@ -2352,6 +2357,7 @@ mod gts_id_dedup_tests { use std::collections::BTreeMap; use std::collections::BTreeSet; use std::sync::Arc; + use toolkit_gts::gts_id; use time::OffsetDateTime; use usage_collector_sdk::{ @@ -2363,9 +2369,9 @@ mod gts_id_dedup_tests { use crate::domain::test_support::{HappyPathPlugin, authenticated_ctx, service_with_permit}; - const GTS_A: &str = "gts.cf.core.uc.usage_record.v1~cf.mini_chat._.tokens_consumed.v1"; - const GTS_B: &str = "gts.cf.core.uc.usage_record.v1~cf.mini_chat._.tokens_emitted.v1"; - const GTS_C: &str = "gts.cf.core.uc.usage_record.v1~cf.mini_chat._.tokens_buffered.v1"; + const GTS_A: &str = gts_id!("cf.core.uc.usage_record.v1~cf.mini_chat._.tokens_consumed.v1"); + const GTS_B: &str = gts_id!("cf.core.uc.usage_record.v1~cf.mini_chat._.tokens_emitted.v1"); + const GTS_C: &str = gts_id!("cf.core.uc.usage_record.v1~cf.mini_chat._.tokens_buffered.v1"); fn counter_usage_type() -> UsageType { UsageType { @@ -2592,6 +2598,7 @@ mod corrects_id_dedup_tests { use std::collections::BTreeMap; use std::collections::BTreeSet; use std::sync::Arc; + use toolkit_gts::gts_id; use time::OffsetDateTime; use usage_collector_sdk::{ @@ -2603,7 +2610,8 @@ mod corrects_id_dedup_tests { use crate::domain::test_support::{HappyPathPlugin, authenticated_ctx, service_with_permit}; - const COUNTER_GTS_ID: &str = "gts.cf.core.uc.usage_record.v1~cf.mini_chat._.tokens_consumed.v1"; + const COUNTER_GTS_ID: &str = + gts_id!("cf.core.uc.usage_record.v1~cf.mini_chat._.tokens_consumed.v1"); fn counter_usage_type() -> UsageType { UsageType { @@ -2895,6 +2903,7 @@ mod corrects_id_dedup_tests { // - Prefetch transient error lifts to `ServiceUnavailable`. mod get_usage_record_tests { use std::sync::Arc; + use toolkit_gts::gts_id; use usage_collector_sdk::{ USAGE_RECORD_RESOURCE, UsageCollectorError, UsageCollectorPluginV1, UsageRecord, @@ -2909,7 +2918,7 @@ mod get_usage_record_tests { }; const HAPPY_RECORD_GTS_ID: &str = - "gts.cf.core.uc.usage_record.v1~cf.mini_chat._.tokens_consumed.v1"; + gts_id!("cf.core.uc.usage_record.v1~cf.mini_chat._.tokens_consumed.v1"); fn sample_persisted_record(uuid: Uuid, tenant_id: Uuid) -> UsageRecord { use std::collections::BTreeMap; @@ -3279,6 +3288,7 @@ mod private_helpers_tests { mod create_usage_record_path_tests { use std::collections::{BTreeMap, BTreeSet}; use std::sync::Arc; + use toolkit_gts::gts_id; use time::OffsetDateTime; use usage_collector_sdk::{ @@ -3294,8 +3304,10 @@ mod create_usage_record_path_tests { service_with_permit, }; - const COUNTER_GTS_ID: &str = "gts.cf.core.uc.usage_record.v1~cf.mini_chat._.tokens_consumed.v1"; - const GAUGE_GTS_ID: &str = "gts.cf.core.uc.usage_record.v1~cf.mini_chat._.tokens_inflight.v1"; + const COUNTER_GTS_ID: &str = + gts_id!("cf.core.uc.usage_record.v1~cf.mini_chat._.tokens_consumed.v1"); + const GAUGE_GTS_ID: &str = + gts_id!("cf.core.uc.usage_record.v1~cf.mini_chat._.tokens_inflight.v1"); fn counter_usage_type() -> UsageType { UsageType { @@ -3695,6 +3707,7 @@ mod create_usage_record_path_tests { mod batch_size_cap_tests { use std::collections::{BTreeMap, BTreeSet}; use std::sync::Arc; + use toolkit_gts::gts_id; use time::OffsetDateTime; use usage_collector_sdk::{ @@ -3706,7 +3719,7 @@ mod batch_size_cap_tests { use crate::domain::service::MAX_BATCH_RECORDS; use crate::domain::test_support::{HappyPathPlugin, authenticated_ctx, service_with_permit}; - const GTS_ID: &str = "gts.cf.core.uc.usage_record.v1~cf.mini_chat._.tokens_consumed.v1"; + const GTS_ID: &str = gts_id!("cf.core.uc.usage_record.v1~cf.mini_chat._.tokens_consumed.v1"); fn counter_usage_type() -> UsageType { UsageType { diff --git a/gears/system/usage-collector/usage-collector/src/domain/validation_tests.rs b/gears/system/usage-collector/usage-collector/src/domain/validation_tests.rs index 3ba6edf63..010e35ede 100644 --- a/gears/system/usage-collector/usage-collector/src/domain/validation_tests.rs +++ b/gears/system/usage-collector/usage-collector/src/domain/validation_tests.rs @@ -1,6 +1,7 @@ //! Unit tests for the shape-validation algorithm. use std::collections::{BTreeMap, BTreeSet}; +use toolkit_gts::gts_id; use rust_decimal::Decimal; use time::OffsetDateTime; @@ -23,8 +24,8 @@ fn mk_keys(values: [&str; N]) -> BTreeSet { values.into_iter().map(mk_key).collect() } -const SAMPLE_COUNTER_ID: &str = "gts.cf.core.uc.usage_record.v1~tenant.example._.foo.v1"; -const SAMPLE_GAUGE_ID: &str = "gts.cf.core.uc.usage_record.v1~tenant.example._.bar.v1"; +const SAMPLE_COUNTER_ID: &str = gts_id!("cf.core.uc.usage_record.v1~tenant.example._.foo.v1"); +const SAMPLE_GAUGE_ID: &str = gts_id!("cf.core.uc.usage_record.v1~tenant.example._.bar.v1"); fn counter_id() -> UsageTypeGtsId { UsageTypeGtsId::new(SAMPLE_COUNTER_ID).expect("valid usage_record-derived id") diff --git a/gears/system/usage-collector/usage-collector/src/gts/permissions.rs b/gears/system/usage-collector/usage-collector/src/gts/permissions.rs index 52f48c816..1fbb13e57 100644 --- a/gears/system/usage-collector/usage-collector/src/gts/permissions.rs +++ b/gears/system/usage-collector/usage-collector/src/gts/permissions.rs @@ -28,7 +28,7 @@ use crate::domain::authz::{usage_record, usage_type}; gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.core.uc.usage_type_create.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.core.uc.usage_type_create.v1"), resource_type: USAGE_TYPE_RESOURCE.to_owned(), action: usage_type::actions::CREATE.to_owned(), display_name: "Create usage type".to_owned(), @@ -36,7 +36,7 @@ gts_instance! { } gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.core.uc.usage_type_get.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.core.uc.usage_type_get.v1"), resource_type: USAGE_TYPE_RESOURCE.to_owned(), action: usage_type::actions::GET.to_owned(), display_name: "Get usage type".to_owned(), @@ -44,7 +44,7 @@ gts_instance! { } gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.core.uc.usage_type_list.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.core.uc.usage_type_list.v1"), resource_type: USAGE_TYPE_RESOURCE.to_owned(), action: usage_type::actions::LIST.to_owned(), display_name: "List usage types".to_owned(), @@ -52,7 +52,7 @@ gts_instance! { } gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.core.uc.usage_type_delete.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.core.uc.usage_type_delete.v1"), resource_type: USAGE_TYPE_RESOURCE.to_owned(), action: usage_type::actions::DELETE.to_owned(), display_name: "Delete usage type".to_owned(), @@ -63,7 +63,7 @@ gts_instance! { gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.core.uc.usage_record_create.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.core.uc.usage_record_create.v1"), resource_type: USAGE_RECORD_RESOURCE.to_owned(), action: usage_record::actions::CREATE.to_owned(), display_name: "Create usage record".to_owned(), @@ -71,7 +71,7 @@ gts_instance! { } gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.core.uc.usage_record_get.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.core.uc.usage_record_get.v1"), resource_type: USAGE_RECORD_RESOURCE.to_owned(), action: usage_record::actions::GET.to_owned(), display_name: "Get usage record".to_owned(), @@ -79,7 +79,7 @@ gts_instance! { } gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.core.uc.usage_record_list.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.core.uc.usage_record_list.v1"), resource_type: USAGE_RECORD_RESOURCE.to_owned(), action: usage_record::actions::LIST.to_owned(), display_name: "List usage records".to_owned(), @@ -87,7 +87,7 @@ gts_instance! { } gts_instance! { AuthzPermissionV1 { - id: "gts.cf.toolkit.authz.permission.v1~cf.core.uc.usage_record_deactivate.v1", + id: gts_id!("cf.toolkit.authz.permission.v1~cf.core.uc.usage_record_deactivate.v1"), resource_type: USAGE_RECORD_RESOURCE.to_owned(), action: usage_record::actions::DEACTIVATE.to_owned(), display_name: "Deactivate usage record".to_owned(), @@ -96,9 +96,9 @@ gts_instance! { #[cfg(test)] mod tests { - use toolkit_gts::InventoryInstance; + use toolkit_gts::{InventoryInstance, gts_id}; - const PERMISSION_TYPE_ID: &str = "gts.cf.toolkit.authz.permission.v1~"; + const PERMISSION_TYPE_ID: &str = gts_id!("cf.toolkit.authz.permission.v1~"); /// Usage-collector's instance-id namespace segment, appended after /// [`PERMISSION_TYPE_ID`]. Kept as a bare fragment (not a `gts.`-prefixed /// literal) so it is composed with the valid type id at the filter site @@ -108,14 +108,14 @@ mod tests { /// One per `(resource_type, action)` the usage-collector REST/PEP surface /// enforces. const EXPECTED_PERMISSION_IDS: &[&str] = &[ - "gts.cf.toolkit.authz.permission.v1~cf.core.uc.usage_type_create.v1", - "gts.cf.toolkit.authz.permission.v1~cf.core.uc.usage_type_get.v1", - "gts.cf.toolkit.authz.permission.v1~cf.core.uc.usage_type_list.v1", - "gts.cf.toolkit.authz.permission.v1~cf.core.uc.usage_type_delete.v1", - "gts.cf.toolkit.authz.permission.v1~cf.core.uc.usage_record_create.v1", - "gts.cf.toolkit.authz.permission.v1~cf.core.uc.usage_record_get.v1", - "gts.cf.toolkit.authz.permission.v1~cf.core.uc.usage_record_list.v1", - "gts.cf.toolkit.authz.permission.v1~cf.core.uc.usage_record_deactivate.v1", + gts_id!("cf.toolkit.authz.permission.v1~cf.core.uc.usage_type_create.v1"), + gts_id!("cf.toolkit.authz.permission.v1~cf.core.uc.usage_type_get.v1"), + gts_id!("cf.toolkit.authz.permission.v1~cf.core.uc.usage_type_list.v1"), + gts_id!("cf.toolkit.authz.permission.v1~cf.core.uc.usage_type_delete.v1"), + gts_id!("cf.toolkit.authz.permission.v1~cf.core.uc.usage_record_create.v1"), + gts_id!("cf.toolkit.authz.permission.v1~cf.core.uc.usage_record_get.v1"), + gts_id!("cf.toolkit.authz.permission.v1~cf.core.uc.usage_record_list.v1"), + gts_id!("cf.toolkit.authz.permission.v1~cf.core.uc.usage_record_deactivate.v1"), ]; fn uc_permission_instances() -> Vec<&'static InventoryInstance> { diff --git a/gears/system/usage-collector/usage-collector/src/infra/sdk_error_mapping.rs b/gears/system/usage-collector/usage-collector/src/infra/sdk_error_mapping.rs index a1e325af4..b0784b925 100644 --- a/gears/system/usage-collector/usage-collector/src/infra/sdk_error_mapping.rs +++ b/gears/system/usage-collector/usage-collector/src/infra/sdk_error_mapping.rs @@ -22,10 +22,10 @@ use usage_collector_sdk::{USAGE_RECORD_RESOURCE, USAGE_TYPE_RESOURCE, UsageColle // - UsageTypeResource → catalog REST surface (create / get / list / delete) // - UsageRecordResource → ingestion REST surface (create / deactivate) -#[resource_error("gts.cf.core.uc.usage_type.v1~")] +#[resource_error(gts_id!("cf.core.uc.usage_type.v1~"))] pub(crate) struct UsageTypeResource; -#[resource_error("gts.cf.core.uc.usage_record.v1~")] +#[resource_error(gts_id!("cf.core.uc.usage_record.v1~"))] pub(crate) struct UsageRecordResource; /// Lift the SDK error envelope onto the AIP-193 canonical shape for the diff --git a/gears/system/usage-collector/usage-collector/src/infra/sdk_error_mapping_tests.rs b/gears/system/usage-collector/usage-collector/src/infra/sdk_error_mapping_tests.rs index 8b8a3922b..1e95f8606 100644 --- a/gears/system/usage-collector/usage-collector/src/infra/sdk_error_mapping_tests.rs +++ b/gears/system/usage-collector/usage-collector/src/infra/sdk_error_mapping_tests.rs @@ -17,7 +17,10 @@ //! Operator triage for 503s reads the curated `detail` string instead. use toolkit_canonical_errors::{CanonicalError, Problem}; -use usage_collector_sdk::{UsageCollectorError, UsageTypeGtsId}; +use toolkit_gts::{GTS_ID_PREFIX, gts_id}; +use usage_collector_sdk::{ + USAGE_RECORD_RESOURCE, USAGE_TYPE_RESOURCE, UsageCollectorError, UsageTypeGtsId, +}; use uuid::Uuid; use super::{ @@ -27,7 +30,7 @@ use super::{ }; const SAMPLE_USAGE_TYPE_ID: &str = - "gts.cf.core.uc.usage_record.v1~cf.mini_chat._.tokens_consumed.v1"; + gts_id!("cf.core.uc.usage_record.v1~cf.mini_chat._.tokens_consumed.v1"); fn sample_gts_id() -> UsageTypeGtsId { UsageTypeGtsId::new(SAMPLE_USAGE_TYPE_ID).expect("valid usage_record-derived usage-type gts_id") @@ -38,7 +41,7 @@ fn usage_type_resource_type_matches_uc_usage_type_marker() { let err: CanonicalError = UsageTypeResource::not_found("x") .with_resource("x") .create(); - assert_eq!(err.resource_type(), Some("gts.cf.core.uc.usage_type.v1~")); + assert_eq!(err.resource_type(), Some(USAGE_TYPE_RESOURCE)); } #[test] @@ -46,7 +49,7 @@ fn usage_record_resource_type_is_record_sibling() { let err: CanonicalError = UsageRecordResource::not_found("r") .with_resource("r") .create(); - assert_eq!(err.resource_type(), Some("gts.cf.core.uc.usage_record.v1~")); + assert_eq!(err.resource_type(), Some(USAGE_RECORD_RESOURCE)); } // --------------------------------------------------------------------------- @@ -59,7 +62,7 @@ fn authorization_from_usage_type_surface_uses_usage_type_resource() { let c = lift_type(UsageCollectorError::permission_denied("denied")); assert_eq!(c.status_code(), 403); assert_eq!(c.title(), "Permission Denied"); - assert_eq!(c.resource_type(), Some("gts.cf.core.uc.usage_type.v1~")); + assert_eq!(c.resource_type(), Some(USAGE_TYPE_RESOURCE)); } #[test] @@ -67,7 +70,7 @@ fn authorization_from_usage_record_surface_uses_usage_record_resource() { let c = lift_record(UsageCollectorError::permission_denied("denied")); assert_eq!(c.status_code(), 403); assert_eq!(c.title(), "Permission Denied"); - assert_eq!(c.resource_type(), Some("gts.cf.core.uc.usage_record.v1~")); + assert_eq!(c.resource_type(), Some(USAGE_RECORD_RESOURCE)); } #[test] @@ -95,7 +98,7 @@ fn invalid_resource_ref_maps_to_400_invalid_argument() { )); assert_eq!(c.status_code(), 400); assert_eq!(c.title(), "Invalid Argument"); - assert_eq!(c.resource_type(), Some("gts.cf.core.uc.usage_record.v1~")); + assert_eq!(c.resource_type(), Some(USAGE_RECORD_RESOURCE)); } #[test] @@ -103,7 +106,7 @@ fn metadata_size_exceeded_maps_to_400_invalid_argument_with_usage_record_resourc let c = lift_record(UsageCollectorError::metadata_size_exceeded(9000, 8192)); assert_eq!(c.status_code(), 400); assert_eq!(c.title(), "Invalid Argument"); - assert_eq!(c.resource_type(), Some("gts.cf.core.uc.usage_record.v1~")); + assert_eq!(c.resource_type(), Some(USAGE_RECORD_RESOURCE)); } #[test] @@ -111,7 +114,7 @@ fn invalid_metadata_field_maps_to_400_invalid_argument() { let c = lift_type(UsageCollectorError::invalid_metadata_field(1, true)); assert_eq!(c.status_code(), 400); assert_eq!(c.title(), "Invalid Argument"); - assert_eq!(c.resource_type(), Some("gts.cf.core.uc.usage_type.v1~")); + assert_eq!(c.resource_type(), Some(USAGE_TYPE_RESOURCE)); } #[test] @@ -119,7 +122,7 @@ fn duplicate_metadata_field_maps_to_400_invalid_argument() { let c = lift_type(UsageCollectorError::duplicate_metadata_field(2)); assert_eq!(c.status_code(), 400); assert_eq!(c.title(), "Invalid Argument"); - assert_eq!(c.resource_type(), Some("gts.cf.core.uc.usage_type.v1~")); + assert_eq!(c.resource_type(), Some(USAGE_TYPE_RESOURCE)); } #[test] @@ -127,7 +130,7 @@ fn invalid_batch_size_maps_to_400_invalid_argument() { let c = lift_record(UsageCollectorError::invalid_batch_size(0, 1, 100)); assert_eq!(c.status_code(), 400); assert_eq!(c.title(), "Invalid Argument"); - assert_eq!(c.resource_type(), Some("gts.cf.core.uc.usage_record.v1~")); + assert_eq!(c.resource_type(), Some(USAGE_RECORD_RESOURCE)); } /// `UnknownMetadataKey` lifts onto `InvalidArgument` (HTTP 400) and identifies @@ -142,7 +145,7 @@ fn unknown_metadata_key_maps_to_invalid_argument() { "unexpected", )); assert_eq!(c.status_code(), 400); - assert_eq!(c.resource_type(), Some("gts.cf.core.uc.usage_type.v1~")); + assert_eq!(c.resource_type(), Some(USAGE_TYPE_RESOURCE)); assert_eq!(c.resource_name(), Some(gts_id.as_ref())); } @@ -151,7 +154,7 @@ fn usage_type_not_found_maps_to_404() { let gts_id = sample_gts_id(); let c = lift_type(UsageCollectorError::usage_type_not_found(>s_id)); assert_eq!(c.status_code(), 404); - assert_eq!(c.resource_type(), Some("gts.cf.core.uc.usage_type.v1~")); + assert_eq!(c.resource_type(), Some(USAGE_TYPE_RESOURCE)); assert_eq!(c.resource_name(), Some(gts_id.as_ref())); } @@ -160,7 +163,7 @@ fn usage_record_not_found_maps_to_404() { let id = Uuid::from_u128(0x1234); let c = lift_record(UsageCollectorError::usage_record_not_found(id)); assert_eq!(c.status_code(), 404); - assert_eq!(c.resource_type(), Some("gts.cf.core.uc.usage_record.v1~")); + assert_eq!(c.resource_type(), Some(USAGE_RECORD_RESOURCE)); } #[test] @@ -168,7 +171,7 @@ fn already_inactive_maps_to_409_aborted_with_already_inactive_reason() { let id = Uuid::from_u128(0xCAFE_BABE); let c = lift_record(UsageCollectorError::already_inactive(id)); assert_eq!(c.status_code(), 409); - assert_eq!(c.resource_type(), Some("gts.cf.core.uc.usage_record.v1~")); + assert_eq!(c.resource_type(), Some(USAGE_RECORD_RESOURCE)); assert_eq!(c.resource_name(), Some(id.to_string().as_str())); let problem = Problem::from(c); @@ -203,7 +206,7 @@ fn idempotency_conflict_maps_to_409_aborted() { existing_uuid, )); assert_eq!(c.status_code(), 409); - assert_eq!(c.resource_type(), Some("gts.cf.core.uc.usage_record.v1~")); + assert_eq!(c.resource_type(), Some(USAGE_RECORD_RESOURCE)); assert_eq!(c.resource_name(), Some(existing_uuid.to_string().as_str())); } @@ -239,12 +242,13 @@ fn non_negative_counter_compensation_carries_semantics_violation_reason() { #[test] fn gauge_compensation_rejected_maps_to_invalid_argument_with_reason() { - let gts_id = - UsageTypeGtsId::new("gts.cf.core.uc.usage_record.v1~tenant.example._.cpu_seconds.v1") - .expect("gauge gts_id"); + let gts_id = UsageTypeGtsId::new(gts_id!( + "cf.core.uc.usage_record.v1~tenant.example._.cpu_seconds.v1" + )) + .expect("gauge gts_id"); let c = lift_record(UsageCollectorError::gauge_compensation_rejected(>s_id)); assert_eq!(c.status_code(), 400); - assert_eq!(c.resource_type(), Some("gts.cf.core.uc.usage_record.v1~")); + assert_eq!(c.resource_type(), Some(USAGE_RECORD_RESOURCE)); let problem = Problem::from(c); assert_eq!( first_field_violation_string(&problem, "reason").as_deref(), @@ -295,7 +299,7 @@ fn corrects_id_targets_compensation_maps_to_409_aborted_with_reason() { corrects_id, )); assert_eq!(c.status_code(), 409); - assert_eq!(c.resource_type(), Some("gts.cf.core.uc.usage_record.v1~")); + assert_eq!(c.resource_type(), Some(USAGE_RECORD_RESOURCE)); let problem = Problem::from(c); assert_eq!( problem_context_string(&problem, "reason").as_deref(), @@ -384,7 +388,7 @@ fn first_field_violation_string(problem: &Problem, key: &str) -> Option #[test] fn invalid_base_gts_id_envelope_carries_field_violation_discriminator() { - let raw = "gts.cf.core.metric.histogram.v1~bogus".to_owned(); + let raw = format!("{GTS_ID_PREFIX}cf.core.metric.histogram.v1~bogus"); let reason = "usage type gts_id must derive from the reserved base \ `gts.cf.core.uc.usage_record.v1~`" .to_owned(); @@ -412,10 +416,7 @@ fn create_usage_type_already_exists_envelope_carries_resource_identity() { let gts_id = sample_gts_id(); let canonical = lift_type(UsageCollectorError::usage_type_already_exists(>s_id)); assert_eq!(canonical.status_code(), 409); - assert_eq!( - canonical.resource_type(), - Some("gts.cf.core.uc.usage_type.v1~") - ); + assert_eq!(canonical.resource_type(), Some(USAGE_TYPE_RESOURCE)); assert_eq!(canonical.resource_name(), Some(gts_id.as_ref())); } @@ -461,7 +462,7 @@ fn invalid_metadata_key_uses_usage_record_resource() { "metadata key must not be empty", )); assert_eq!(c.status_code(), 400); - assert_eq!(c.resource_type(), Some("gts.cf.core.uc.usage_record.v1~")); + assert_eq!(c.resource_type(), Some(USAGE_RECORD_RESOURCE)); } // --------------------------------------------------------------------------- diff --git a/guidelines/GTS.md b/guidelines/GTS.md index ee32d7521..46eff8e63 100644 --- a/guidelines/GTS.md +++ b/guidelines/GTS.md @@ -40,6 +40,7 @@ - [Example: before and after](#example-before-and-after) - [When string constants are still appropriate](#when-string-constants-are-still-appropriate) - [6.7 Heterogeneous Dispatch — `GtsSchema for Value` and `try_narrow`](#67-heterogeneous-dispatch--gtsschema-for-value-and-try_narrow) + - [6.8 Compile-Time Prefix Customization (`GTS_ID_PREFIX`)](#68-compile-time-prefix-customization-gts_id_prefix) - [7. Base Types — Design Guidelines](#7-base-types--design-guidelines) - [8. GTS-Based Security and Wildcard Access Control](#8-gts-based-security-and-wildcard-access-control) - [8.1 Why Naming Structure Matters for Security](#81-why-naming-structure-matters-for-security) @@ -78,7 +79,7 @@ GTS brings the following capabilities to Gears: 3. **Wildcard-based access control (ABAC)** — grant or restrict permissions using patterns like `gts.cf.core.events.type.v1~acme.*` instead of maintaining explicit resource lists, can be useful for cross-vendor data isolation 4. **Extensible (derived) types** — third-party vendors safely extend platform base types via inheritance chains while maintaining compatibility guarantees 5. **Hybrid storage pattern** — store base-type fields in indexed DB columns, vendor-specific extensions in JSONB — no schema migrations needed for new type variants -6. **Code-generated JSON Schema contracts** — GTS Type Schemas generated from Rust structs via `#[struct_to_gts_schema]`, keeping type definitions in code rather than hand-maintained JSON +6. **Code-generated JSON Schema contracts** — GTS Type Schemas generated from Rust structs via `#[gts_type_schema]` (the `toolkit_gts` wrapper around upstream `#[struct_to_gts_schema]`), keeping type definitions in code rather than hand-maintained JSON 7. **Globally unique identifiers across vendors** — no naming collisions; deterministic UUID v5 derivation from any GTS identifier for compact, fixed-size DB storage and external system interop 8. **Semantic trait metadata attached to type schemas** — cross-cutting properties (e.g., event routing topic, retention policy, audit requirements) declared as structured values in `x-gts-traits`, with their shape/defaults defined by `x-gts-traits-schema`, inherited and merged across the derivation chain 9. **GTS Registry** — GTS Type Schemas and well-known Instances indexed by GTS Identifier for discovery, validation, compatibility checking, and plugin resolution @@ -342,7 +343,9 @@ This is the core database pattern that GTS enables. A gear stores base-type fiel ```rust // gts-rust deterministic UUID derivation -let type_uuid = gts::GtsID::new("gts.cf.core.events.type.v1~")?.to_uuid(); +use toolkit_gts::gts_id; + +let type_uuid = gts::GtsID::new(gts_id!("cf.core.events.type.v1~"))?.to_uuid(); // Always produces the same UUID for the same GTS string ``` @@ -497,28 +500,48 @@ The `$id` uses the `gts://` URI prefix. The `$ref` references the base type. The ## 6. GTS in Rust Code -**Upstream naming has shifted to `type_id` terminology.** In new Rust code and in PR reviews, prefer `type_id`, `TYPE_ID`, `BASE_TYPE_ID`, `gts_type_id()`, `gts_base_type_id()`, `innermost_type_id()`, and `GtsTypeId`. Treat `schema_id`, `SCHEMA_ID`, `BASE_SCHEMA_ID`, `gts_schema_id()`, and `GtsSchemaId` as deprecated compatibility aliases unless you are touching legacy call sites. `GtsSchemaId` is now a deprecated type alias (`pub type GtsSchemaId = GtsTypeId`). The `schema_id = "..."` macro attribute emits a compile-time deprecation warning — new code must use `type_id = "..."`. Specifying both `type_id` and `schema_id` on the same struct is a hard compile error. +**Upstream naming has shifted to `type_id` terminology.** In new Rust code and in PR reviews, prefer `type_id`, `TYPE_ID`, `BASE_TYPE_ID`, `gts_type_id()`, `gts_base_type_id()`, `innermost_type_id()`, and `GtsTypeId`. Treat `schema_id`, `SCHEMA_ID`, `BASE_SCHEMA_ID`, `gts_schema_id()`, and `GtsSchemaId` as deprecated compatibility aliases unless you are touching legacy call sites. `GtsSchemaId` is now a deprecated type alias (`pub type GtsSchemaId = GtsTypeId`). The `schema_id = "..."` macro attribute emits a compile-time deprecation warning — new code must use `type_id = "..."` (or `type_id = gts_id!("...")`). Specifying both `type_id` and `schema_id` on the same struct is a hard compile error. In gts-rust result types, note the distinction between two predicates: `is_type_schema` (entity results — is this entity a GTS Type Schema, not an instance?) vs `is_type` (parse/validate ID results — does this GTS Identifier end with `~`, making it a type rather than an instance ID?). -Gears generate GTS type schemas from Rust structs using the `#[struct_to_gts_schema]` macro. This keeps type definitions in code, not in hand-maintained JSON files. +Gears generate GTS type schemas from Rust structs using the `#[struct_to_gts_schema]` macro (wrapped as `#[gts_type_schema]` via `toolkit_gts`). This keeps type definitions in code, not in hand-maintained JSON files. + +**GTS identifier literals must use the `gts_id!` / `gts_uri!` macros.** As of gts-rust v0.11.0, the `gts.` prefix is no longer hardcoded — it is a compile-time configurable constant (`GTS_ID_PREFIX`, default `"gts."`). All GTS identifier string literals in Rust code must be written through the `gts_id!` macro, which prepends the prefix automatically. The `gts_uri!` macro does the same for URI-form identifiers (`gts://`). Both macros are re-exported from `toolkit_gts`. + +```rust +use toolkit_gts::gts_id; + +// Suffix without "gts." prefix — the macro adds it +const TYPE_ID: &str = gts_id!("cf.core.toolkit.plugin.v1~"); +// Expands to: "gts.cf.core.toolkit.plugin.v1~" + +// Already-prefixed literals are passed through unchanged +const ALSO_OK: &str = gts_id!("gts.cf.core.toolkit.plugin.v1~"); + +// URI form for $id / $ref in JSON schemas and RFC 9457 type URIs +use toolkit_gts::gts_uri; +const SCHEMA_URI: &str = gts_uri!("cf.core.events.type.v1~"); +// Expands to: "gts://gts.cf.core.events.type.v1~" +``` + +The same `gts_id!("...")` form is accepted inside `type_id = ...`, `id: ...`, and `"id": ...` arguments of `#[gts_type_schema]`, `gts_instance!`, and `gts_instance_raw!`. Always write the suffix **without** the `gts.` prefix in these positions. ### 6.1 Defining a Base Type ```rust -// libs/toolkit/src/gts/plugin.rs +// libs/toolkit-gts/src/plugin.rs use gts::GtsInstanceId; -use gts_macros::struct_to_gts_schema; +use toolkit_gts::gts_type_schema; #[derive(Debug)] -#[struct_to_gts_schema( +#[gts_type_schema( dir_path = "schemas", base = true, - type_id = "gts.cf.core.toolkit.plugin.v1~", + type_id = gts_id!("cf.core.toolkit.plugin.v1~"), description = "Base toolkit plugin schema", properties = "id,vendor,priority,properties" )] -pub struct BaseToolkitPluginV1 { +pub struct PluginV1 { pub id: GtsInstanceId, pub vendor: String, pub priority: i16, @@ -538,25 +561,25 @@ Using `String` bypasses structural validation and makes the origin of the value ```rust // gears/credstore/credstore-sdk/src/gts.rs -use gts_macros::struct_to_gts_schema; -use toolkit::gts::BaseToolkitPluginV1; +use toolkit_gts::gts_type_schema; +use toolkit_gts::PluginV1; -#[struct_to_gts_schema( +#[gts_type_schema( dir_path = "schemas", - base = BaseToolkitPluginV1, - type_id = "gts.cf.core.toolkit.plugin.v1~cf.core.credstore.plugin.v1~", + base = PluginV1, + type_id = gts_id!("cf.core.toolkit.plugin.v1~cf.core.credstore.plugin.v1~"), description = "CredStore plugin specification", properties = "" )] pub struct CredStorePluginSpecV1; ``` -The `base = BaseToolkitPluginV1` links this derived type to its base. The `type_id` is a chained GTS identifier showing the inheritance: `base~derived~`. +The `base = PluginV1` links this derived type to its base. The `type_id` is a chained GTS identifier showing the inheritance: `base~derived~`. -**Review note — generated schema shape matters:** when PRs add or modify generic or nested derived GTS structs, review the generated schema artifact or tests, not just the Rust type. Derived overlays must be wrapped at the full nesting path under the intended extension field; otherwise fields can leak to the top level and accidentally violate `additionalProperties: false` contracts. The `description` attribute is now emitted into the generated JSON schema root — a PR that adds or changes `description` on a `#[struct_to_gts_schema]` struct must also update the checked-in schema artifact. +**Review note — generated schema shape matters:** when PRs add or modify generic or nested derived GTS structs, review the generated schema artifact or tests, not just the Rust type. Derived overlays must be wrapped at the full nesting path under the intended extension field; otherwise fields can leak to the top level and accidentally violate `additionalProperties: false` contracts. The `description` attribute is now emitted into the generated JSON schema root — a PR that adds or changes `description` on a `#[gts_type_schema]` struct must also update the checked-in schema artifact. **Review note — type schema definition checklist:** -- `type_id = "..."` attribute ends with `~`; use `type_id` not the deprecated `schema_id` +- `type_id = gts_id!("...")` attribute ends with `~`; use `type_id` not the deprecated `schema_id` - For base types: `base = true`; for derived types: `base = ParentStruct` (not `base = true`) — the macro validates at compile time that the `type_id` prefix matches `::TYPE_ID` - `description` attribute is present — it is now emitted into the schema JSON root - Struct fields that reference other GTS types use `GtsTypeId` / `GtsInstanceId`, not `String` @@ -599,7 +622,7 @@ async fn init(&self, ctx: &GearCtx) -> anyhow::Result<()> { - The `segment` argument does **not** end with `~` — instance identifiers are not type identifiers - The struct field holding the instance ID uses `GtsInstanceId`, not `String` - The instance payload body must **not** contain schema-only keywords (`x-gts-traits`, `x-gts-traits-schema`, `x-gts-final`, `x-gts-abstract`) — the library rejects such payloads at registration with a structured error -- The type used as the instance's schema must itself be defined via `#[struct_to_gts_schema]` before any instances are registered +- The type used as the instance's schema must itself be defined via `#[gts_type_schema]` before any instances are registered ### 6.4 Defining GTS Constants @@ -607,16 +630,17 @@ For well-known types and instances, define constants in a dedicated `gts_helpers ```rust // gears/system/oagw/oagw/src/domain/gts_helpers.rs +use toolkit_gts::gts_id; -// Schema GTS identifiers (types) -pub const UPSTREAM_SCHEMA: &str = "gts.cf.core.oagw.upstream.v1~"; -pub const ROUTE_SCHEMA: &str = "gts.cf.core.oagw.route.v1~"; -pub const AUTH_PLUGIN_SCHEMA: &str = "gts.cf.core.oagw.auth_plugin.v1~"; +// Schema GTS identifiers (types) — suffix without "gts." prefix +pub const UPSTREAM_SCHEMA: &str = gts_id!("cf.core.oagw.upstream.v1~"); +pub const ROUTE_SCHEMA: &str = gts_id!("cf.core.oagw.route.v1~"); +pub const AUTH_PLUGIN_SCHEMA: &str = gts_id!("cf.core.oagw.auth_plugin.v1~"); // Builtin instances (well-known — no trailing ~) -pub const HTTP_PROTOCOL_ID: &str = "gts.cf.core.oagw.protocol.v1~cf.core.oagw.http.v1"; -pub const APIKEY_AUTH_ID: &str = "gts.cf.core.oagw.auth_plugin.v1~cf.core.oagw.apikey.v1"; -pub const BEARER_AUTH_ID: &str = "gts.cf.core.oagw.auth_plugin.v1~cf.core.oagw.bearer.v1"; +pub const HTTP_PROTOCOL_ID: &str = gts_id!("cf.core.oagw.protocol.v1~cf.core.oagw.http.v1"); +pub const APIKEY_AUTH_ID: &str = gts_id!("cf.core.oagw.auth_plugin.v1~cf.core.oagw.apikey.v1"); +pub const BEARER_AUTH_ID: &str = gts_id!("cf.core.oagw.auth_plugin.v1~cf.core.oagw.bearer.v1"); // Format an anonymous instance (UUID-based) pub fn format_upstream_gts(id: Uuid) -> String { @@ -643,16 +667,18 @@ Avoid `format!` to build GTS identifiers at runtime — typed constructors valid GTS identifiers are used as the `type` URI in RFC 9457 Problem responses: ```rust +use toolkit_gts::gts_uri; + impl From for Problem { fn from(e: SlessOrchestratorError) -> Self { match e { SlessOrchestratorError::NotFound { id } => Problem::not_found() - .with_type_uri("gts://gts.cf.core.sless.err.v1~cf.core.sless.err.not_found.v1~") + .with_type_uri(gts_uri!("cf.core.sless.err.v1~cf.core.sless.err.not_found.v1~")) .with_detail(format!("Function not found: {id}")), SlessOrchestratorError::RateLimited { retry_after_seconds, .. } => Problem::too_many_requests() - .with_type_uri("gts://gts.cf.core.sless.err.v1~cf.core.sless.err.rate_limited.v1~") + .with_type_uri(gts_uri!("cf.core.sless.err.v1~cf.core.sless.err.rate_limited.v1~")) .with_header("Retry-After", retry_after_seconds), // ... } @@ -832,6 +858,90 @@ match gts_type.as_ref() { - Deserializing macro-generated nested GTS types directly with `serde::from_value` — use `try_narrow`, which correctly routes through `GtsDeserializeWrapper` - Comparing runtime discriminator strings to raw string literals instead of `::innermost_type_id()` +### 6.8 Compile-Time Prefix Customization (`GTS_ID_PREFIX`) + +By default, all GTS identifiers start with `gts.`. Since gts-rust v0.11.0, this prefix is **not hardcoded** — it is a compile-time constant (`GTS_ID_PREFIX`) resolved from the `GTS_ID_PREFIX` environment variable, defaulting to `"gts."` when unset. This allows an organization to use a custom prefix (e.g. `acme.`) across the entire codebase without modifying any source code. + +#### How it works + +The `gts-id` crate resolves the prefix at compile time via `option_env!("GTS_ID_PREFIX")` with a `const` validation function. The value is baked into the binary — a single binary cannot work with multiple prefixes. The crate's `build.rs` emits `cargo:rerun-if-env-changed=GTS_ID_PREFIX` so Cargo automatically rebuilds when the variable changes. + +The `gts_id!` and `gts_uri!` proc-macros (re-exported from `toolkit_gts`) read `gts_id::GTS_ID_PREFIX` at macro-expansion time, so all GTS identifier literals in the codebase automatically use the configured prefix. **No source code changes are required** — as long as all identifiers are written via `gts_id!("suffix")` (without a hardcoded `gts.` prefix), the entire binary switches to the custom prefix. + +#### Setting a custom prefix + +```bash +# Build with a custom prefix +GTS_ID_PREFIX=acme. cargo build + +# Run tests with a custom prefix +GTS_ID_PREFIX=acme. cargo test + +# CI / deployment +GTS_ID_PREFIX=acme. cargo build --release +``` + +With `GTS_ID_PREFIX=acme.`, the macros expand as follows: + +```rust +use toolkit_gts::gts_id; + +// Suffix written without prefix — the macro prepends the configured prefix +const TYPE_ID: &str = gts_id!("cf.core.toolkit.plugin.v1~"); +// Expands to: "acme.cf.core.toolkit.plugin.v1~" (instead of "gts.cf.core.toolkit.plugin.v1~") + +// Already-prefixed literals are detected and passed through unchanged +const ALSO_OK: &str = gts_id!("acme.cf.core.toolkit.plugin.v1~"); +// Expands to: "acme.cf.core.toolkit.plugin.v1~" (no doubling) +``` + +#### Prefix validation rules + +The prefix is validated at compile time — an invalid value fails the build with a clear diagnostic. A valid prefix is a single lowercase token (`[a-z][a-z0-9_]*`) terminated by a single `.`: + +| Value | Accepted? | Reason | +|-------|-----------|--------| +| `gts.` | ✅ | The default | +| `acme.` | ✅ | Single lowercase token + trailing dot | +| `acme` | ❌ | Missing trailing `.` | +| `Acme.` | ❌ | Uppercase rejected | +| `my.org.` | ❌ | Multi-segment (contains an extra `.`) | +| `acme-prod.` | ❌ | Hyphen is not allowed | +| `_.` | ❌ | Must start with a letter, not underscore | +| (empty) | ❌ | Empty prefix | + +#### Relevant constants + +| Constant | Source | Description | +|----------|--------|-------------| +| `GTS_ID_PREFIX` | `gts_id` / `toolkit_gts` | The configured prefix (default `"gts."`). | +| `DEFAULT_GTS_ID_PREFIX` | `gts_id` | The default prefix (`"gts."`), regardless of override. | +| `GTS_ID_PREFIX_ENV` | `gts_id` | The env var name (`"GTS_ID_PREFIX"`). | +| `GTS_ID_URI_PREFIX` | `gts` / `toolkit_gts` | The URI prefix (`"gts://"`), not affected by `GTS_ID_PREFIX`. | + +#### What you need to do + +- **Always write GTS identifiers via `gts_id!("suffix")`** — never hardcode the `gts.` prefix in string literals +- **Write suffixes without the prefix** — `gts_id!("cf.core.toolkit.plugin.v1~")`, not `gts_id!("gts.cf.core.toolkit.plugin.v1~")` +- **Use `GTS_ID_PREFIX` constant** (from `toolkit_gts`) when you need to reference the prefix at runtime, e.g. for string manipulation or validation +- **Set the env var for the entire build** — since the prefix is baked into the binary at compile time, all crates in the dependency graph must be compiled with the same `GTS_ID_PREFIX` value. Cargo handles this automatically via `rerun-if-env-changed` + +#### Testing the customization + +The workspace includes tests that verify prefix customization works end-to-end: + +```bash +# Default prefix +cargo test -p cf-gears-toolkit-gts --test prefix_customization -- --nocapture +# Output: Compiled with GTS_ID_PREFIX = "gts." + +# Custom prefix +GTS_ID_PREFIX=acme. cargo test -p cf-gears-toolkit-gts --test prefix_customization -- --nocapture +# Output: Compiled with GTS_ID_PREFIX = "acme." +``` + +Both runs pass — the tests check structural invariants (prefix prepended, suffix preserved, URI form correct) rather than hardcoding `"gts."`. + --- ## 7. Base Types — Design Guidelines @@ -1059,7 +1169,7 @@ Types that **must not be extended**. No derived types are allowed. 1. **Vendor prefix**: all Constructor Fabric- defined base types use `cf` as vendor (include the Gears types) 2. **SDK placement**: GTS type definitions live in `-sdk/src/gts.rs` -3. **Schema generation**: use `#[struct_to_gts_schema]` macro — do not maintain schemas by hand +3. **Schema generation**: use `#[gts_type_schema]` macro (from `toolkit_gts`) — do not maintain schemas by hand 4. **Constants**: well-known GTS identifiers are defined as `const` strings in `domain/gts_helpers.rs` or `gts.rs` 5. **Registration**: plugins register GTS instances in the GTS Registry during `init()` 6. **DB storage**: base fields in columns, extension data in `JSONB` or `TEXT` @@ -1067,7 +1177,8 @@ Types that **must not be extended**. No derived types are allowed. 8. **Access control**: structure identifiers so that wildcard policies can grant/revoke access at the vendor, package, or namespace level 9. **Dylint enforcement**: GTS-specific lints validate identifier correctness and prevent unsupported patterns at compile time 10. **Constants as GTS instances**: discriminator fields and string constants that select behavior, routing, or authorization should be GTS well-known instances — not raw strings or Rust enums (see [section 6.6](#66-gts-well-known-instances-for-constants-and-discriminator-values)) -11. **Rust naming**: in new code prefer `type_id`/`TYPE_ID`/`GtsTypeId` naming; treat `schema_id` names as deprecated compatibility aliases. The `schema_id = "..."` macro attribute produces a compile-time deprecation warning — use `type_id = "..."` instead +11. **Rust naming**: in new code prefer `type_id`/`TYPE_ID`/`GtsTypeId` naming; treat `schema_id` names as deprecated compatibility aliases. The `schema_id = "..."` macro attribute produces a compile-time deprecation warning — use `type_id = "..."` (or `type_id = gts_id!("...")`) instead +16. **GTS identifier literals**: use `gts_id!("")` for all GTS identifier string literals in Rust code — the `gts.` prefix is prepended at compile time from the configurable `GTS_ID_PREFIX`. Use `gts_uri!("")` for URI-form identifiers (`gts://`). Both macros are re-exported from `toolkit_gts`. The same `gts_id!("...")` form is accepted inside `type_id`/`id`/`"id"` arguments of `#[gts_type_schema]`, `gts_instance!`, and `gts_instance_raw!` — always write the suffix without the `gts.` prefix 12. **Schema dialect**: handwritten GTS JSON Schemas and fixtures must target Draft-07 (`"$schema": "http://json-schema.org/draft-07/schema#"`) and avoid post-Draft-07 keywords; use `definitions` (not `$defs`) for local reusable subschemas 13. **Heterogeneous carriers**: when a type must hold multiple concrete GTS leaf types at runtime, use `serde_json::Value` as the generic parameter with `try_narrow` for typed dispatch (see [section 6.7](#67-heterogeneous-dispatch--gtsschema-for-value-and-try_narrow)) 14. **Typed GTS ID fields**: Rust struct fields that carry GTS identifiers use `GtsTypeId` (for type refs ending with `~`) or `GtsInstanceId` (for instance refs) — never plain `String` or `&str` @@ -1149,7 +1260,8 @@ When reviewing Rust code, verify not just naming correctness but also that the c **Rust review checks:** - Prefer canonical naming: `type_id`, `TYPE_ID`, `BASE_TYPE_ID`, `gts_type_id()`, `GtsTypeId` -- Use `#[struct_to_gts_schema]` for GTS schema generation instead of hand-maintained JSON where possible +- Use `gts_id!("...")` / `gts_uri!("...")` for all GTS identifier string literals — never hardcode the `gts.` prefix in Rust code +- Use `#[gts_type_schema]` (from `toolkit_gts`) for GTS schema generation instead of hand-maintained JSON where possible - Ensure generated schemas use Draft-07 semantics and GTS URI forms for `$id` / `$ref` - Validate boundary fields as full GTS Type Identifiers ending with `~` - Keep schema-only keywords out of instance documents @@ -1165,13 +1277,13 @@ When reviewing Rust code, verify not just naming correctness but also that the c - If the PR introduces a `Base` heterogeneous carrier, is `try_narrow` used for dispatch, and are discriminators compared via `::innermost_type_id()` rather than raw string literals? **High-risk Rust changes to inspect carefully:** -- Macro attribute changes on `#[struct_to_gts_schema]` +- Macro attribute changes on `#[gts_type_schema]` / `#[struct_to_gts_schema]` - Renames touching `type_id` fields, constants, or generated methods - Changes to JSON Schema emission, `allOf` structure, nested generic paths, or `$ref` generation - Changes to validation/parsing at API boundaries - Changes that allow instances to carry schema-only keywords - Changes that replace GTS-backed values with raw strings or enums -- Changes to `description` attribute values on `#[struct_to_gts_schema]` structs — verify the checked-in schema artifact reflects the new value in the JSON root +- Changes to `description` attribute values on a `#[gts_type_schema]` struct — verify the checked-in schema artifact reflects the new value in the JSON root - New `Base` carrier patterns without a corresponding `try_narrow` dispatch site **Common Rust anti-patterns:** @@ -1198,6 +1310,7 @@ When reviewing Rust code, verify not just naming correctness but also that the c - [ ] If JSON Schema is handwritten, does it use Draft-07 (`http://json-schema.org/draft-07/schema#`) and avoid post-Draft-07 keywords such as `$defs`? - [ ] Do type-reference fields at boundaries validate a full GTS Type Identifier ending with `~`, not just a `gts.` prefix? - [ ] Do Rust struct fields that carry GTS identifiers use `GtsTypeId` (type refs) or `GtsInstanceId` (instance refs), not `String`? +- [ ] Are all GTS identifier string literals written via `gts_id!("...")` / `gts_uri!("...")` — no hardcoded `gts.` prefix in Rust code? - [ ] For a new GTS type: does `type_id` end with `~`, and for derived types does `base = ParentStruct` correctly reflect the parent chain? - [ ] Are instance IDs constructed via `gts_make_instance_id()`, not via string concatenation or `format!`? - [ ] Does the instance payload omit all schema-only keywords (`x-gts-traits`, `x-gts-traits-schema`, `x-gts-final`, `x-gts-abstract`)? @@ -1205,7 +1318,7 @@ When reviewing Rust code, verify not just naming correctness but also that the c - [ ] Are GTS constants defined in Rust code, not magic strings scattered across handlers? - [ ] In new Rust code, are canonical names used (`type_id`, `TYPE_ID`, `GtsTypeId`, `gts_type_id()`), with deprecated `schema_id` aliases avoided unless needed for compatibility? - [ ] If this PR changes generic or nested derived structs, do generated schema artifacts/tests prove the overlay lands at the correct full nesting path? -- [ ] If `description` was added or changed on a `#[struct_to_gts_schema]` struct, does the checked-in schema artifact reflect the update in the JSON root? +- [ ] If `description` was added or changed on a `#[gts_type_schema]` struct, does the checked-in schema artifact reflect the update in the JSON root? - [ ] If the PR introduces a `Base` heterogeneous carrier, is `try_narrow` used for dispatch and are `NarrowError` variants handled? - [ ] Should any string-constant discriminator fields use GTS instances instead? (see [section 6.6](#66-gts-well-known-instances-for-constants-and-discriminator-values)) - [ ] Do discriminator schemas carry `properties` that the engine can read instead of `match` arms? diff --git a/libs/toolkit-canonical-errors-macro/Cargo.toml b/libs/toolkit-canonical-errors-macro/Cargo.toml index 58e5dfbf7..ce44fbb59 100644 --- a/libs/toolkit-canonical-errors-macro/Cargo.toml +++ b/libs/toolkit-canonical-errors-macro/Cargo.toml @@ -23,3 +23,7 @@ proc-macro-crate = { workspace = true } proc-macro2 = { workspace = true } quote = { workspace = true } syn = { workspace = true } +# For validating the GTS resource type prefix in `#[resource_error("...")]` +# without hard-coding the literal prefix. The prefix is the compile-time +# configured one (`gts_id::GTS_ID_PREFIX`). +gts-id = { workspace = true } diff --git a/libs/toolkit-canonical-errors-macro/src/lib.rs b/libs/toolkit-canonical-errors-macro/src/lib.rs index 7f67c932f..d299b940e 100644 --- a/libs/toolkit-canonical-errors-macro/src/lib.rs +++ b/libs/toolkit-canonical-errors-macro/src/lib.rs @@ -1,7 +1,7 @@ #![cfg_attr(coverage_nightly, feature(coverage_attribute))] //! Proc-macro for canonical error resource types. //! -//! Provides the `#[resource_error("gts...")]` attribute macro. +//! Provides the `#[resource_error(...)]` attribute macro. use proc_macro::TokenStream; use proc_macro2::{Span, TokenStream as TokenStream2}; @@ -18,7 +18,7 @@ use syn::parse_macro_input; /// ```rust,ignore /// use toolkit_canonical_errors::resource_error; /// -/// #[resource_error("gts.cf.core.users.user.v1~")] +/// #[resource_error(gts_id!("cf.core.users.user.v1~"))] /// struct UserResourceError; /// ``` /// @@ -29,7 +29,47 @@ use syn::parse_macro_input; /// typestate enforcement. #[proc_macro_attribute] pub fn resource_error(attr: TokenStream, item: TokenStream) -> TokenStream { - let gts_lit = parse_macro_input!(attr as LitStr); + // Accept either a plain full-id string literal + // or the `gts_id!("")` marker form to avoid hard-coding the + // configured GTS ID prefix. The marker form is expanded here by + // prepending `gts_id::GTS_ID_PREFIX` (the compile-time configured + // prefix); a plain literal is taken verbatim and must already include + // the full prefix. + let attr_ts: proc_macro2::TokenStream = attr.into(); + let gts_lit: LitStr = match syn::parse2::(attr_ts) { + Ok(syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(s), + .. + })) => s, + Ok(syn::Expr::Macro(syn::ExprMacro { mac, .. })) + if mac + .path + .segments + .last() + .is_some_and(|seg| seg.ident == "gts_id") => + { + let suffix: LitStr = if let Ok(s) = mac.parse_body() { + s + } else { + let err = syn::Error::new_spanned( + mac, + "`gts_id!` takes a single string-literal suffix, \ + e.g. `gts_id!(\"cf.core.users.user.v1~\")`", + ); + return err.to_compile_error().into(); + }; + LitStr::new( + &format!("{}{}", gts_id::GTS_ID_PREFIX, suffix.value()), + suffix.span(), + ) + } + Ok(other) => { + let err = + syn::Error::new_spanned(other, "expected a string literal or `gts_id!(\"...\")`"); + return err.to_compile_error().into(); + } + Err(e) => return e.to_compile_error().into(), + }; let input = parse_macro_input!(item as syn::ItemStruct); match generate_resource_error(>s_lit, &input) { @@ -246,83 +286,20 @@ fn generate_resource_error(gts_lit: &LitStr, input: &syn::ItemStruct) -> syn::Re }) } -/// Validates a GTS resource-type literal at proc-macro time. +/// Validates a GTS resource-type literal at proc-macro time by delegating +/// to the canonical `GtsId` parser from the `gts-id` crate. /// -/// Expected format: `gts.....~` +/// This is equivalent to `GtsTypeId::try_new` from the `gts` crate (which +/// internally calls `GtsId::try_new` + `is_type()`), but avoids pulling the +/// heavy `gts` crate (jsonschema, schemars, …) into a proc-macro. fn validate_gts_resource_type_str(s: &str, span: Span) -> syn::Result<()> { - let b = s.as_bytes(); - let len = b.len(); - - if len == 0 { - return Err(syn::Error::new(span, "GTS resource type must not be empty")); - } - - if b[len - 1] != b'~' { - return Err(syn::Error::new(span, "GTS resource type must end with '~'")); - } - - #[allow(unknown_lints)] - #[allow(de0901_gts_string_pattern)] - if len < 6 || !s.starts_with("gts.") { - return Err(syn::Error::new( - span, - "GTS resource type must start with 'gts.'", - )); - } - - let body = &s[4..len - 1]; - if body.is_empty() { - return Err(syn::Error::new( - span, - "GTS resource type must have segments after 'gts.' prefix", - )); - } - - let segments: Vec<&str> = body.split('.').collect(); - - for seg in &segments { - if seg.is_empty() { - return Err(syn::Error::new( - span, - "GTS resource type contains an empty segment", - )); - } - if !seg - .bytes() - .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == b'_') - { - return Err(syn::Error::new( - span, - "GTS resource type segments must contain only lowercase ASCII letters, digits, or underscores", - )); - } - } - - // Need >= 5 segments: vendor.package.namespace.type.version - if segments.len() < 5 { + let parsed = gts_id::GtsId::try_new(s) + .map_err(|e| syn::Error::new(span, format!("invalid GTS resource type: {e}")))?; + if !parsed.is_type() { return Err(syn::Error::new( span, - "GTS resource type must have at least 5 segments after 'gts.': vendor.package.namespace.type.version", + "GTS resource type must end with '~' (type id, not instance id)", )); } - - // Version segment validation - // SAFETY: segments.len() >= 5 is checked above, so `.last()` is always `Some`. - let Some(version) = segments.last() else { - unreachable!() - }; - if !version.starts_with('v') || version.len() < 2 { - return Err(syn::Error::new( - span, - "GTS resource type must end with a version segment starting with 'v' (e.g. v1)", - )); - } - if !version[1..].bytes().all(|c| c.is_ascii_digit()) { - return Err(syn::Error::new( - span, - "GTS resource type version segment after 'v' must contain only ASCII digits", - )); - } - Ok(()) } diff --git a/libs/toolkit-canonical-errors/Cargo.toml b/libs/toolkit-canonical-errors/Cargo.toml index b24154dcf..90b902027 100644 --- a/libs/toolkit-canonical-errors/Cargo.toml +++ b/libs/toolkit-canonical-errors/Cargo.toml @@ -34,7 +34,7 @@ http = { workspace = true, optional = true } utoipa = { workspace = true, optional = true } tracing = { workspace = true, optional = true } toolkit-canonical-errors-macro = { workspace = true } +toolkit-gts = { workspace = true } [dev-dependencies] -gts-id = { workspace = true } trybuild = { workspace = true } diff --git a/libs/toolkit-canonical-errors/src/error.rs b/libs/toolkit-canonical-errors/src/error.rs index 6e3122439..fe7509d2c 100644 --- a/libs/toolkit-canonical-errors/src/error.rs +++ b/libs/toolkit-canonical-errors/src/error.rs @@ -1,5 +1,12 @@ use std::fmt; +// `gts_id!` is the prefix-free GTS-id construction macro (re-exported at the +// crate root from `gts_macros`). It expands at compile time to a `&'static +// str` literal with the configured `GTS_ID_PREFIX` +// prepended, so the canonical error type ids below track the configured +// prefix without hard-coding the literal prefix. +use crate::gts_id; + use crate::context::{ Aborted, AlreadyExists, Cancelled, DataLoss, DeadlineExceeded, FailedPrecondition, Internal, InvalidArgument, NotFound, OutOfRange, PermissionDenied, ResourceExhausted, ServiceUnavailable, @@ -466,37 +473,41 @@ impl CanonicalError { #[must_use] pub fn gts_type(&self) -> &'static str { match self { - Self::Cancelled { .. } => "gts.cf.core.errors.err.v1~cf.core.err.cancelled.v1~", - Self::Unknown { .. } => "gts.cf.core.errors.err.v1~cf.core.err.unknown.v1~", + Self::Cancelled { .. } => gts_id!("cf.core.errors.err.v1~cf.core.err.cancelled.v1~"), + Self::Unknown { .. } => gts_id!("cf.core.errors.err.v1~cf.core.err.unknown.v1~"), Self::InvalidArgument { .. } => { - "gts.cf.core.errors.err.v1~cf.core.err.invalid_argument.v1~" + gts_id!("cf.core.errors.err.v1~cf.core.err.invalid_argument.v1~") } Self::DeadlineExceeded { .. } => { - "gts.cf.core.errors.err.v1~cf.core.err.deadline_exceeded.v1~" + gts_id!("cf.core.errors.err.v1~cf.core.err.deadline_exceeded.v1~") } - Self::NotFound { .. } => "gts.cf.core.errors.err.v1~cf.core.err.not_found.v1~", + Self::NotFound { .. } => gts_id!("cf.core.errors.err.v1~cf.core.err.not_found.v1~"), Self::AlreadyExists { .. } => { - "gts.cf.core.errors.err.v1~cf.core.err.already_exists.v1~" + gts_id!("cf.core.errors.err.v1~cf.core.err.already_exists.v1~") } Self::PermissionDenied { .. } => { - "gts.cf.core.errors.err.v1~cf.core.err.permission_denied.v1~" + gts_id!("cf.core.errors.err.v1~cf.core.err.permission_denied.v1~") } Self::ResourceExhausted { .. } => { - "gts.cf.core.errors.err.v1~cf.core.err.resource_exhausted.v1~" + gts_id!("cf.core.errors.err.v1~cf.core.err.resource_exhausted.v1~") } Self::FailedPrecondition { .. } => { - "gts.cf.core.errors.err.v1~cf.core.err.failed_precondition.v1~" + gts_id!("cf.core.errors.err.v1~cf.core.err.failed_precondition.v1~") + } + Self::Aborted { .. } => gts_id!("cf.core.errors.err.v1~cf.core.err.aborted.v1~"), + Self::OutOfRange { .. } => { + gts_id!("cf.core.errors.err.v1~cf.core.err.out_of_range.v1~") + } + Self::Unimplemented { .. } => { + gts_id!("cf.core.errors.err.v1~cf.core.err.unimplemented.v1~") } - Self::Aborted { .. } => "gts.cf.core.errors.err.v1~cf.core.err.aborted.v1~", - Self::OutOfRange { .. } => "gts.cf.core.errors.err.v1~cf.core.err.out_of_range.v1~", - Self::Unimplemented { .. } => "gts.cf.core.errors.err.v1~cf.core.err.unimplemented.v1~", - Self::Internal { .. } => "gts.cf.core.errors.err.v1~cf.core.err.internal.v1~", + Self::Internal { .. } => gts_id!("cf.core.errors.err.v1~cf.core.err.internal.v1~"), Self::ServiceUnavailable { .. } => { - "gts.cf.core.errors.err.v1~cf.core.err.service_unavailable.v1~" + gts_id!("cf.core.errors.err.v1~cf.core.err.service_unavailable.v1~") } - Self::DataLoss { .. } => "gts.cf.core.errors.err.v1~cf.core.err.data_loss.v1~", + Self::DataLoss { .. } => gts_id!("cf.core.errors.err.v1~cf.core.err.data_loss.v1~"), Self::Unauthenticated { .. } => { - "gts.cf.core.errors.err.v1~cf.core.err.unauthenticated.v1~" + gts_id!("cf.core.errors.err.v1~cf.core.err.unauthenticated.v1~") } } } diff --git a/libs/toolkit-canonical-errors/src/lib.rs b/libs/toolkit-canonical-errors/src/lib.rs index 6ff4af34b..cef59a82c 100644 --- a/libs/toolkit-canonical-errors/src/lib.rs +++ b/libs/toolkit-canonical-errors/src/lib.rs @@ -18,3 +18,9 @@ pub use context::{ pub use error::CanonicalError; pub use problem::{Problem, ProblemConversionError}; pub use toolkit_canonical_errors_macro::resource_error; +// Re-export the `gts_id!` helper so consumers using `#[resource_error(...)]` +// can write `#[resource_error(gts_id!("cf.core.users.user.v1~"))]` without +// adding a separate `gts-macros` dependency. The macro expands at compile +// time to a `&'static str` literal with the configured GTS ID prefix +// prepended (overridable via `GTS_ID_PREFIX`). +pub use toolkit_gts::gts_id; diff --git a/libs/toolkit-canonical-errors/src/problem.rs b/libs/toolkit-canonical-errors/src/problem.rs index 5704afc47..c464d3b9b 100644 --- a/libs/toolkit-canonical-errors/src/problem.rs +++ b/libs/toolkit-canonical-errors/src/problem.rs @@ -1,5 +1,11 @@ use serde::{Deserialize, Serialize}; +// `GTS_ID_PREFIX` is the compile-time configured GTS identifier prefix +// (overridable via the `GTS_ID_PREFIX` env var at build +// time). Used to assemble the canonical error type id prefix without +// hard-coding the literal prefix. +use toolkit_gts::{GTS_ID_PREFIX, GTS_ID_URI_PREFIX, gts_uri}; + use crate::context::{ Aborted, AlreadyExists, Cancelled, DataLoss, DeadlineExceeded, FailedPrecondition, Internal, InvalidArgument, NotFound, OutOfRange, PermissionDenied, ResourceExhausted, ServiceUnavailable, @@ -38,7 +44,7 @@ impl Problem { /// should never fail, but this keeps the failure visible rather than /// silently producing an empty `"context": {}`. pub fn from_error(err: &CanonicalError) -> Result { - let problem_type = format!("gts://{}", err.gts_type()); + let problem_type = gts_uri!(err.gts_type()); let title = err.title().to_owned(); let status = err.status_code(); let detail = err.detail().to_owned(); @@ -133,7 +139,7 @@ impl From for Problem { match Problem::from_error(&err) { Ok(p) => p, Err(ser_err) => Problem { - problem_type: format!("gts://{}", err.gts_type()), + problem_type: gts_uri!(err.gts_type()), title: err.title().to_owned(), status: err.status_code(), detail: err.detail().to_owned(), @@ -164,7 +170,7 @@ impl From for Problem { // --------------------------------------------------------------------------- /// Prefix on `Problem.problem_type` produced by the forward conversion. -const PROBLEM_TYPE_PREFIX: &str = "gts://"; +const PROBLEM_TYPE_PREFIX: &str = GTS_ID_URI_PREFIX; /// Reasons a `Problem` cannot be reconstructed as a `CanonicalError`. #[derive(Debug, thiserror::Error)] @@ -191,15 +197,28 @@ pub enum ProblemConversionError { /// itself — only the concatenation `{prefix}{category}{suffix}` is a valid /// GTS identifier. #[allow(unknown_lints, de0901_gts_string_pattern)] -const GTS_TYPE_PREFIX: &str = "gts.cf.core.errors.err.v1~cf.core.err."; -/// Suffix of every canonical GTS identifier. See [`GTS_TYPE_PREFIX`]. +/// Prefix of every canonical GTS error type id. Built by concatenating the +/// configured GTS ID prefix (overridable via `GTS_ID_PREFIX` +/// at compile time) with the fixed "cf.core.errors.err.v1~cf.core.err." +/// suffix. This is *not* a complete GTS id (the trailing per-error token is +/// appended at runtime in `CanonicalError::gts_type`), so `gts_id!` (which +/// validates a full id at macro-expansion time) is not applicable here. +/// `concat!` also cannot be used since it only accepts literals; the value +/// is therefore materialised once via a `OnceLock` and exposed as a +/// `&'static str`. +fn gts_type_prefix() -> &'static str { + use std::sync::OnceLock; + static PREFIX: OnceLock = OnceLock::new(); + PREFIX.get_or_init(|| format!("{GTS_ID_PREFIX}cf.core.errors.err.v1~cf.core.err.")) +} +/// Suffix of every canonical GTS identifier. See [`gts_type_prefix`]. const GTS_TYPE_SUFFIX: &str = ".v1~"; -/// Strip `gts://gts.cf.core.errors.err.v1~cf.core.err..v1~` down to +/// Strip the canonical problem-type URI down to /// ``. Returns `None` if the URI doesn't match the canonical shape. fn category_from_problem_type(problem_type: &str) -> Option<&str> { let rest = problem_type.strip_prefix(PROBLEM_TYPE_PREFIX)?; - let after_prefix = rest.strip_prefix(GTS_TYPE_PREFIX)?; + let after_prefix = rest.strip_prefix(gts_type_prefix())?; after_prefix.strip_suffix(GTS_TYPE_SUFFIX) } @@ -376,7 +395,12 @@ impl axum::response::IntoResponse for Problem { status = self.status, "failed to serialize Problem; emitting fallback body", ); - let body: &[u8] = br#"{"type":"gts://gts.cf.core.errors.err.v1~cf.core.err.internal.v1~","title":"Internal","status":500,"detail":"failed to serialize problem","context":{}}"#; + let body = format!( + r#"{{"type":"{}{}internal{}","title":"Internal","status":500,"detail":"failed to serialize problem","context":{{}}}}"#, + PROBLEM_TYPE_PREFIX, + gts_type_prefix(), + GTS_TYPE_SUFFIX + ); ( http::StatusCode::INTERNAL_SERVER_ERROR, [(http::header::CONTENT_TYPE, APPLICATION_PROBLEM_JSON)], diff --git a/libs/toolkit-canonical-errors/tests/axum_integration.rs b/libs/toolkit-canonical-errors/tests/axum_integration.rs index 70c202054..6df88a9a2 100644 --- a/libs/toolkit-canonical-errors/tests/axum_integration.rs +++ b/libs/toolkit-canonical-errors/tests/axum_integration.rs @@ -5,7 +5,7 @@ use toolkit_canonical_errors::problem::APPLICATION_PROBLEM_JSON; use toolkit_canonical_errors::resource_error; use toolkit_canonical_errors::{CanonicalError, Problem}; -#[resource_error("gts.cf.core.test.axum.v1~")] +#[resource_error(gts_id!("cf.core.test.axum.v1~"))] struct AxumTestR; #[test] diff --git a/libs/toolkit-canonical-errors/tests/error.rs b/libs/toolkit-canonical-errors/tests/error.rs index af63e2801..e8bb300f9 100644 --- a/libs/toolkit-canonical-errors/tests/error.rs +++ b/libs/toolkit-canonical-errors/tests/error.rs @@ -2,8 +2,9 @@ extern crate toolkit_canonical_errors; use toolkit_canonical_errors::resource_error; use toolkit_canonical_errors::{CanonicalError, Problem}; +use toolkit_gts::{GtsId, gts_id}; -#[resource_error("gts.cf.core.users.user.v1~")] +#[resource_error(gts_id!("cf.core.users.user.v1~"))] struct R; #[test] @@ -13,7 +14,7 @@ fn not_found_gts_type() { .create(); assert_eq!( err.gts_type(), - "gts.cf.core.errors.err.v1~cf.core.err.not_found.v1~" + gts_id!("cf.core.errors.err.v1~cf.core.err.not_found.v1~") ); } @@ -110,7 +111,7 @@ fn from_io_error_produces_internal() { assert_eq!(err.title(), "Internal"); assert_eq!( err.gts_type(), - "gts.cf.core.errors.err.v1~cf.core.err.internal.v1~" + gts_id!("cf.core.errors.err.v1~cf.core.err.internal.v1~") ); } @@ -126,7 +127,7 @@ fn from_serde_json_error_produces_invalid_argument() { assert_eq!(err.detail(), raw_msg); assert_eq!( err.gts_type(), - "gts.cf.core.errors.err.v1~cf.core.err.invalid_argument.v1~" + gts_id!("cf.core.errors.err.v1~cf.core.err.invalid_argument.v1~") ); assert_eq!(err.diagnostic(), None); } @@ -153,7 +154,7 @@ fn question_mark_propagation_serde_json() { assert_eq!(err.status_code(), 400); assert_eq!( err.gts_type(), - "gts.cf.core.errors.err.v1~cf.core.err.invalid_argument.v1~" + gts_id!("cf.core.errors.err.v1~cf.core.err.invalid_argument.v1~") ); } @@ -202,7 +203,6 @@ fn validate_all_gts_ids() { for err in &errors { let id = err.gts_type(); assert!(id.ends_with('~'), "GTS type ID must end with ~: {id}"); - gts_id::validate_gts_id(id, false) - .unwrap_or_else(|e| panic!("Invalid GTS type ID '{id}': {e}")); + GtsId::try_new(id).unwrap_or_else(|e| panic!("Invalid GTS type ID '{id}': {e}")); } } diff --git a/libs/toolkit-canonical-errors/tests/problem.rs b/libs/toolkit-canonical-errors/tests/problem.rs index 37f02b2ab..ac9b51665 100644 --- a/libs/toolkit-canonical-errors/tests/problem.rs +++ b/libs/toolkit-canonical-errors/tests/problem.rs @@ -2,11 +2,15 @@ extern crate toolkit_canonical_errors; use toolkit_canonical_errors::resource_error; use toolkit_canonical_errors::{CanonicalError, Problem}; +use toolkit_gts::{gts_id, gts_uri}; -#[resource_error("gts.cf.core.users.user.v1~")] +const USER_RESOURCE: &str = gts_id!("cf.core.users.user.v1~"); +const FUTURE_PROBLEM_TYPE: &str = gts_uri!("cf.future.errors.something_new.v1~"); + +#[resource_error(gts_id!("cf.core.users.user.v1~"))] struct R; -#[resource_error("gts.cf.core.test.resource.v1~")] +#[resource_error(gts_id!("cf.core.test.resource.v1~"))] struct TestR; #[test] @@ -17,15 +21,12 @@ fn problem_from_not_found_has_correct_fields() { let problem = Problem::from(err); assert_eq!( problem.problem_type, - "gts://gts.cf.core.errors.err.v1~cf.core.err.not_found.v1~" + gts_uri!("cf.core.errors.err.v1~cf.core.err.not_found.v1~") ); assert_eq!(problem.title, "Not Found"); assert_eq!(problem.status, 404); assert_eq!(problem.detail, "Resource not found"); - assert_eq!( - problem.context["resource_type"], - "gts.cf.core.users.user.v1~" - ); + assert_eq!(problem.context["resource_type"], USER_RESOURCE); assert_eq!(problem.context["resource_name"], "user-123"); } @@ -160,7 +161,7 @@ fn round_trip_invalid_argument_with_field_violation() { .. } => { assert_eq!(detail, "Request validation failed"); - assert_eq!(resource_type.as_deref(), Some("gts.cf.core.users.user.v1~")); + assert_eq!(resource_type.as_deref(), Some(USER_RESOURCE)); assert!(resource_name.is_none()); match ctx { toolkit_canonical_errors::InvalidArgument::FieldViolations { field_violations } => { @@ -203,7 +204,7 @@ fn round_trip_not_found_preserves_resource_type_and_name() { .. } => { assert_eq!(detail, "User not found"); - assert_eq!(resource_type.as_deref(), Some("gts.cf.core.users.user.v1~")); + assert_eq!(resource_type.as_deref(), Some(USER_RESOURCE)); assert_eq!(resource_name.as_deref(), Some("user-42")); } other => panic!("expected NotFound, got {other:?}"), @@ -466,7 +467,7 @@ fn round_trip_data_loss() { #[test] fn try_from_unknown_problem_type_returns_error() { let problem = Problem { - problem_type: "gts://gts.cf.future.errors.something_new.v1~".to_owned(), + problem_type: FUTURE_PROBLEM_TYPE.to_owned(), title: "Future".to_owned(), status: 599, detail: "Not in canonical taxonomy".to_owned(), @@ -477,7 +478,7 @@ fn try_from_unknown_problem_type_returns_error() { let result = CanonicalError::try_from(problem); match result { Err(ProblemConversionError::UnknownProblemType(t)) => { - assert_eq!(t, "gts://gts.cf.future.errors.something_new.v1~"); + assert_eq!(t, FUTURE_PROBLEM_TYPE); } other => panic!("expected UnknownProblemType, got {other:?}"), } @@ -506,7 +507,7 @@ fn try_from_malformed_context_returns_error() { // deserialize into the expected ResourceExhausted shape — `violations` // must be an array, not a number. let problem = Problem { - problem_type: "gts://gts.cf.core.errors.err.v1~cf.core.err.resource_exhausted.v1~" + problem_type: gts_uri!("cf.core.errors.err.v1~cf.core.err.resource_exhausted.v1~") .to_owned(), title: "Resource Exhausted".to_owned(), status: 429, @@ -539,7 +540,7 @@ fn round_trip_preserves_resource_type_when_context_is_empty_struct() { resource_name, .. } => { - assert_eq!(resource_type.as_deref(), Some("gts.cf.core.users.user.v1~")); + assert_eq!(resource_type.as_deref(), Some(USER_RESOURCE)); assert_eq!(resource_name.as_deref(), Some("session-abc")); } other => panic!("expected DeadlineExceeded, got {other:?}"), diff --git a/libs/toolkit-canonical-errors/tests/resource_error_macro.rs b/libs/toolkit-canonical-errors/tests/resource_error_macro.rs index ce4ebf223..4749d898f 100644 --- a/libs/toolkit-canonical-errors/tests/resource_error_macro.rs +++ b/libs/toolkit-canonical-errors/tests/resource_error_macro.rs @@ -2,8 +2,14 @@ extern crate toolkit_canonical_errors; use toolkit_canonical_errors::Problem; use toolkit_canonical_errors::resource_error; +use toolkit_gts::gts_id; -#[resource_error("gts.cf.core.users.user.v1~")] +const USER_RESOURCE: &str = gts_id!("cf.core.users.user.v1~"); +const NOT_FOUND_ERROR: &str = gts_id!("cf.core.errors.err.v1~cf.core.err.not_found.v1~"); +const PERMISSION_DENIED_ERROR: &str = + gts_id!("cf.core.errors.err.v1~cf.core.err.permission_denied.v1~"); + +#[resource_error(gts_id!("cf.core.users.user.v1~"))] struct TestUserResourceError; #[test] @@ -11,16 +17,10 @@ fn macro_not_found_has_correct_resource_type_and_resource_info() { let err = TestUserResourceError::not_found("User not found") .with_resource("user-123") .create(); - assert_eq!(err.resource_type(), Some("gts.cf.core.users.user.v1~")); - assert_eq!( - err.gts_type(), - "gts.cf.core.errors.err.v1~cf.core.err.not_found.v1~" - ); + assert_eq!(err.resource_type(), Some(USER_RESOURCE)); + assert_eq!(err.gts_type(), NOT_FOUND_ERROR); let problem = Problem::from(err); - assert_eq!( - problem.context["resource_type"], - "gts.cf.core.users.user.v1~" - ); + assert_eq!(problem.context["resource_type"], USER_RESOURCE); assert_eq!(problem.context["resource_name"], "user-123"); } @@ -29,11 +29,8 @@ fn macro_permission_denied_has_correct_resource_type() { let err = TestUserResourceError::permission_denied() .with_reason("INSUFFICIENT_ROLE") .create(); - assert_eq!(err.resource_type(), Some("gts.cf.core.users.user.v1~")); - assert_eq!( - err.gts_type(), - "gts.cf.core.errors.err.v1~cf.core.err.permission_denied.v1~" - ); + assert_eq!(err.resource_type(), Some(USER_RESOURCE)); + assert_eq!(err.gts_type(), PERMISSION_DENIED_ERROR); } #[test] @@ -43,8 +40,5 @@ fn problem_json_includes_resource_type_when_set() { .create(); let problem = Problem::from(err); let json = serde_json::to_value(&problem).unwrap(); - assert_eq!( - json["context"]["resource_type"], - "gts.cf.core.users.user.v1~" - ); + assert_eq!(json["context"]["resource_type"], USER_RESOURCE); } diff --git a/libs/toolkit-canonical-errors/tests/showcase.rs b/libs/toolkit-canonical-errors/tests/showcase.rs index 02ea0e23e..b2640a5e4 100644 --- a/libs/toolkit-canonical-errors/tests/showcase.rs +++ b/libs/toolkit-canonical-errors/tests/showcase.rs @@ -2,17 +2,23 @@ extern crate toolkit_canonical_errors; use toolkit_canonical_errors::resource_error; use toolkit_canonical_errors::{CanonicalError, Problem}; +use toolkit_gts::{gts_id, gts_uri}; -#[resource_error("gts.cf.core.users.user.v1~")] +const USER_RESOURCE: &str = gts_id!("cf.core.users.user.v1~"); +const FILE_RESOURCE: &str = gts_id!("cf.core.files.file.v1~"); +const TENANT_RESOURCE: &str = gts_id!("cf.core.tenants.tenant.v1~"); +const UPSTREAM_RESOURCE: &str = gts_id!("cf.oagw.upstreams.upstream.v1~"); + +#[resource_error(gts_id!("cf.core.users.user.v1~"))] struct UserResourceError; -#[resource_error("gts.cf.core.files.file.v1~")] +#[resource_error(gts_id!("cf.core.files.file.v1~"))] struct FileResourceError; -#[resource_error("gts.cf.core.tenants.tenant.v1~")] +#[resource_error(gts_id!("cf.core.tenants.tenant.v1~"))] struct TenantResourceError; -#[resource_error("gts.cf.oagw.upstreams.upstream.v1~")] +#[resource_error(gts_id!("cf.oagw.upstreams.upstream.v1~"))] struct UpstreamResourceError; // ========================================================================= @@ -30,12 +36,12 @@ fn showcase_not_found() { assert_eq!( json, serde_json::json!({ - "type": "gts://gts.cf.core.errors.err.v1~cf.core.err.not_found.v1~", + "type": gts_uri!("cf.core.errors.err.v1~cf.core.err.not_found.v1~"), "title": "Not Found", "status": 404, "detail": "User not found", "context": { - "resource_type": "gts.cf.core.users.user.v1~", + "resource_type": USER_RESOURCE, "resource_name": "user-123" } }) @@ -53,12 +59,12 @@ fn showcase_already_exists() { assert_eq!( json, serde_json::json!({ - "type": "gts://gts.cf.core.errors.err.v1~cf.core.err.already_exists.v1~", + "type": gts_uri!("cf.core.errors.err.v1~cf.core.err.already_exists.v1~"), "title": "Already Exists", "status": 409, "detail": "User already exists", "context": { - "resource_type": "gts.cf.core.users.user.v1~", + "resource_type": USER_RESOURCE, "resource_name": "alice@example.com" } }) @@ -76,12 +82,12 @@ fn showcase_data_loss() { assert_eq!( json, serde_json::json!({ - "type": "gts://gts.cf.core.errors.err.v1~cf.core.err.data_loss.v1~", + "type": gts_uri!("cf.core.errors.err.v1~cf.core.err.data_loss.v1~"), "title": "Data Loss", "status": 500, "detail": "Data loss detected", "context": { - "resource_type": "gts.cf.core.files.file.v1~", + "resource_type": FILE_RESOURCE, "resource_name": "01JFILE-ABC" } }) @@ -101,12 +107,12 @@ fn showcase_invalid_argument() { assert_eq!( json, serde_json::json!({ - "type": "gts://gts.cf.core.errors.err.v1~cf.core.err.invalid_argument.v1~", + "type": gts_uri!("cf.core.errors.err.v1~cf.core.err.invalid_argument.v1~"), "title": "Invalid Argument", "status": 400, "detail": "Request validation failed", "context": { - "resource_type": "gts.cf.core.users.user.v1~", + "resource_type": USER_RESOURCE, "field_violations": [ { "field": "email", @@ -136,12 +142,12 @@ fn showcase_invalid_argument_format() { assert_eq!( json, serde_json::json!({ - "type": "gts://gts.cf.core.errors.err.v1~cf.core.err.invalid_argument.v1~", + "type": gts_uri!("cf.core.errors.err.v1~cf.core.err.invalid_argument.v1~"), "title": "Invalid Argument", "status": 400, "detail": "Request body is not valid JSON", "context": { - "resource_type": "gts.cf.core.users.user.v1~", + "resource_type": USER_RESOURCE, "format": "Request body is not valid JSON" } }) @@ -160,12 +166,12 @@ fn showcase_invalid_argument_constraint() { assert_eq!( json, serde_json::json!({ - "type": "gts://gts.cf.core.errors.err.v1~cf.core.err.invalid_argument.v1~", + "type": gts_uri!("cf.core.errors.err.v1~cf.core.err.invalid_argument.v1~"), "title": "Invalid Argument", "status": 400, "detail": "at most 10 tags allowed per resource", "context": { - "resource_type": "gts.cf.core.users.user.v1~", + "resource_type": USER_RESOURCE, "constraint": "at most 10 tags allowed per resource" } }) @@ -185,12 +191,12 @@ fn showcase_invalid_argument_format_with_resource() { assert_eq!( json, serde_json::json!({ - "type": "gts://gts.cf.core.errors.err.v1~cf.core.err.invalid_argument.v1~", + "type": gts_uri!("cf.core.errors.err.v1~cf.core.err.invalid_argument.v1~"), "title": "Invalid Argument", "status": 400, "detail": "Request body is not valid JSON", "context": { - "resource_type": "gts.cf.core.users.user.v1~", + "resource_type": USER_RESOURCE, "resource_name": "user-123", "format": "Request body is not valid JSON" } @@ -213,12 +219,12 @@ fn showcase_out_of_range() { assert_eq!( json, serde_json::json!({ - "type": "gts://gts.cf.core.errors.err.v1~cf.core.err.out_of_range.v1~", + "type": gts_uri!("cf.core.errors.err.v1~cf.core.err.out_of_range.v1~"), "title": "Out of Range", "status": 400, "detail": "Page out of range", "context": { - "resource_type": "gts.cf.core.users.user.v1~", + "resource_type": USER_RESOURCE, "field_violations": [ { "field": "page", @@ -242,12 +248,12 @@ fn showcase_permission_denied() { assert_eq!( json, serde_json::json!({ - "type": "gts://gts.cf.core.errors.err.v1~cf.core.err.permission_denied.v1~", + "type": gts_uri!("cf.core.errors.err.v1~cf.core.err.permission_denied.v1~"), "title": "Permission Denied", "status": 403, "detail": "You do not have permission to perform this operation", "context": { - "resource_type": "gts.cf.core.tenants.tenant.v1~", + "resource_type": TENANT_RESOURCE, "reason": "CROSS_TENANT_ACCESS" } }) @@ -265,12 +271,12 @@ fn showcase_aborted() { assert_eq!( json, serde_json::json!({ - "type": "gts://gts.cf.core.errors.err.v1~cf.core.err.aborted.v1~", + "type": gts_uri!("cf.core.errors.err.v1~cf.core.err.aborted.v1~"), "title": "Aborted", "status": 409, "detail": "Operation aborted due to concurrency conflict", "context": { - "resource_type": "gts.cf.oagw.upstreams.upstream.v1~", + "resource_type": UPSTREAM_RESOURCE, "reason": "OPTIMISTIC_LOCK_FAILURE" } }) @@ -286,12 +292,12 @@ fn showcase_unimplemented() { assert_eq!( json, serde_json::json!({ - "type": "gts://gts.cf.core.errors.err.v1~cf.core.err.unimplemented.v1~", + "type": gts_uri!("cf.core.errors.err.v1~cf.core.err.unimplemented.v1~"), "title": "Unimplemented", "status": 501, "detail": "This operation is not implemented", "context": { - "resource_type": "gts.cf.core.users.user.v1~" + "resource_type": USER_RESOURCE } }) ); @@ -312,12 +318,12 @@ fn showcase_failed_precondition() { assert_eq!( json, serde_json::json!({ - "type": "gts://gts.cf.core.errors.err.v1~cf.core.err.failed_precondition.v1~", + "type": gts_uri!("cf.core.errors.err.v1~cf.core.err.failed_precondition.v1~"), "title": "Failed Precondition", "status": 400, "detail": "Operation precondition not met", "context": { - "resource_type": "gts.cf.core.tenants.tenant.v1~", + "resource_type": TENANT_RESOURCE, "violations": [ { "type": "STATE", @@ -339,7 +345,7 @@ fn showcase_internal() { assert_eq!( json, serde_json::json!({ - "type": "gts://gts.cf.core.errors.err.v1~cf.core.err.internal.v1~", + "type": gts_uri!("cf.core.errors.err.v1~cf.core.err.internal.v1~"), "title": "Internal", "status": 500, "detail": "An internal error occurred. Please retry later.", @@ -358,12 +364,12 @@ fn showcase_deadline_exceeded() { assert_eq!( json, serde_json::json!({ - "type": "gts://gts.cf.core.errors.err.v1~cf.core.err.deadline_exceeded.v1~", + "type": gts_uri!("cf.core.errors.err.v1~cf.core.err.deadline_exceeded.v1~"), "title": "Deadline Exceeded", "status": 504, "detail": "Request timed out", "context": { - "resource_type": "gts.cf.core.users.user.v1~" + "resource_type": USER_RESOURCE } }) ); @@ -378,12 +384,12 @@ fn showcase_cancelled() { assert_eq!( json, serde_json::json!({ - "type": "gts://gts.cf.core.errors.err.v1~cf.core.err.cancelled.v1~", + "type": gts_uri!("cf.core.errors.err.v1~cf.core.err.cancelled.v1~"), "title": "Cancelled", "status": 499, "detail": "Operation cancelled by the client", "context": { - "resource_type": "gts.cf.core.users.user.v1~" + "resource_type": USER_RESOURCE } }) ); @@ -404,7 +410,7 @@ fn showcase_unauthenticated() { assert_eq!( json, serde_json::json!({ - "type": "gts://gts.cf.core.errors.err.v1~cf.core.err.unauthenticated.v1~", + "type": gts_uri!("cf.core.errors.err.v1~cf.core.err.unauthenticated.v1~"), "title": "Unauthenticated", "status": 401, "detail": "Authentication required", @@ -429,12 +435,12 @@ fn showcase_resource_exhausted() { assert_eq!( json, serde_json::json!({ - "type": "gts://gts.cf.core.errors.err.v1~cf.core.err.resource_exhausted.v1~", + "type": gts_uri!("cf.core.errors.err.v1~cf.core.err.resource_exhausted.v1~"), "title": "Resource Exhausted", "status": 429, "detail": "Quota exceeded", "context": { - "resource_type": "gts.cf.core.users.user.v1~", + "resource_type": USER_RESOURCE, "violations": [ { "subject": "requests_per_minute", @@ -458,7 +464,7 @@ fn showcase_unavailable() { assert_eq!( json, serde_json::json!({ - "type": "gts://gts.cf.core.errors.err.v1~cf.core.err.service_unavailable.v1~", + "type": gts_uri!("cf.core.errors.err.v1~cf.core.err.service_unavailable.v1~"), "title": "Service Unavailable", "status": 503, "detail": "Service temporarily unavailable", @@ -478,12 +484,12 @@ fn showcase_unknown() { assert_eq!( json, serde_json::json!({ - "type": "gts://gts.cf.core.errors.err.v1~cf.core.err.unknown.v1~", + "type": gts_uri!("cf.core.errors.err.v1~cf.core.err.unknown.v1~"), "title": "Unknown", "status": 500, "detail": "An unknown error occurred", "context": { - "resource_type": "gts.cf.core.users.user.v1~" + "resource_type": USER_RESOURCE } }) ); diff --git a/libs/toolkit-canonical-errors/tests/ui/missing_field_violation_on_invalid_argument.rs b/libs/toolkit-canonical-errors/tests/ui/missing_field_violation_on_invalid_argument.rs index 97da6801a..5ea2ea3a1 100644 --- a/libs/toolkit-canonical-errors/tests/ui/missing_field_violation_on_invalid_argument.rs +++ b/libs/toolkit-canonical-errors/tests/ui/missing_field_violation_on_invalid_argument.rs @@ -2,7 +2,7 @@ extern crate toolkit_canonical_errors; use toolkit_canonical_errors::resource_error; -#[resource_error("gts.cf.core.users.user.v1~")] +#[resource_error(gts_id!("cf.core.users.user.v1~"))] struct UserResourceError; fn main() { diff --git a/libs/toolkit-canonical-errors/tests/ui/missing_precondition_violation_on_failed_precondition.rs b/libs/toolkit-canonical-errors/tests/ui/missing_precondition_violation_on_failed_precondition.rs index a0f528015..11ad9926c 100644 --- a/libs/toolkit-canonical-errors/tests/ui/missing_precondition_violation_on_failed_precondition.rs +++ b/libs/toolkit-canonical-errors/tests/ui/missing_precondition_violation_on_failed_precondition.rs @@ -2,7 +2,7 @@ extern crate toolkit_canonical_errors; use toolkit_canonical_errors::resource_error; -#[resource_error("gts.cf.core.users.user.v1~")] +#[resource_error(gts_id!("cf.core.users.user.v1~"))] struct UserResourceError; fn main() { diff --git a/libs/toolkit-canonical-errors/tests/ui/missing_quota_violation_on_resource_exhausted.rs b/libs/toolkit-canonical-errors/tests/ui/missing_quota_violation_on_resource_exhausted.rs index 63b142c1c..976ab4b11 100644 --- a/libs/toolkit-canonical-errors/tests/ui/missing_quota_violation_on_resource_exhausted.rs +++ b/libs/toolkit-canonical-errors/tests/ui/missing_quota_violation_on_resource_exhausted.rs @@ -2,7 +2,7 @@ extern crate toolkit_canonical_errors; use toolkit_canonical_errors::resource_error; -#[resource_error("gts.cf.core.users.user.v1~")] +#[resource_error(gts_id!("cf.core.users.user.v1~"))] struct UserResourceError; fn main() { diff --git a/libs/toolkit-canonical-errors/tests/ui/missing_reason_on_aborted.rs b/libs/toolkit-canonical-errors/tests/ui/missing_reason_on_aborted.rs index 85b8eceb1..5b003d275 100644 --- a/libs/toolkit-canonical-errors/tests/ui/missing_reason_on_aborted.rs +++ b/libs/toolkit-canonical-errors/tests/ui/missing_reason_on_aborted.rs @@ -2,7 +2,7 @@ extern crate toolkit_canonical_errors; use toolkit_canonical_errors::resource_error; -#[resource_error("gts.cf.core.users.user.v1~")] +#[resource_error(gts_id!("cf.core.users.user.v1~"))] struct UserResourceError; fn main() { diff --git a/libs/toolkit-canonical-errors/tests/ui/missing_reason_on_out_of_range.rs b/libs/toolkit-canonical-errors/tests/ui/missing_reason_on_out_of_range.rs index 50bfe4748..0185ac17b 100644 --- a/libs/toolkit-canonical-errors/tests/ui/missing_reason_on_out_of_range.rs +++ b/libs/toolkit-canonical-errors/tests/ui/missing_reason_on_out_of_range.rs @@ -2,7 +2,7 @@ extern crate toolkit_canonical_errors; use toolkit_canonical_errors::resource_error; -#[resource_error("gts.cf.core.users.user.v1~")] +#[resource_error(gts_id!("cf.core.users.user.v1~"))] struct UserResourceError; fn main() { diff --git a/libs/toolkit-canonical-errors/tests/ui/missing_reason_on_permission_denied.rs b/libs/toolkit-canonical-errors/tests/ui/missing_reason_on_permission_denied.rs index 1d2c83acc..47c773efd 100644 --- a/libs/toolkit-canonical-errors/tests/ui/missing_reason_on_permission_denied.rs +++ b/libs/toolkit-canonical-errors/tests/ui/missing_reason_on_permission_denied.rs @@ -2,7 +2,7 @@ extern crate toolkit_canonical_errors; use toolkit_canonical_errors::resource_error; -#[resource_error("gts.cf.core.users.user.v1~")] +#[resource_error(gts_id!("cf.core.users.user.v1~"))] struct UserResourceError; fn main() { diff --git a/libs/toolkit-canonical-errors/tests/ui/missing_resource_on_not_found.rs b/libs/toolkit-canonical-errors/tests/ui/missing_resource_on_not_found.rs index d2dfa0dba..235fdd502 100644 --- a/libs/toolkit-canonical-errors/tests/ui/missing_resource_on_not_found.rs +++ b/libs/toolkit-canonical-errors/tests/ui/missing_resource_on_not_found.rs @@ -2,7 +2,7 @@ extern crate toolkit_canonical_errors; use toolkit_canonical_errors::resource_error; -#[resource_error("gts.cf.core.users.user.v1~")] +#[resource_error(gts_id!("cf.core.users.user.v1~"))] struct UserResourceError; fn main() { diff --git a/libs/toolkit-gts-macros/Cargo.toml b/libs/toolkit-gts-macros/Cargo.toml index 149db7f7f..7721b782d 100644 --- a/libs/toolkit-gts-macros/Cargo.toml +++ b/libs/toolkit-gts-macros/Cargo.toml @@ -24,3 +24,7 @@ proc-macro-crate = { workspace = true } proc-macro2 = { workspace = true } quote = { workspace = true } syn = { workspace = true } +# For the configured GTS_ID_PREFIX used when expanding the `gts_id!("...")` +# marker form inside `type_id = ...` / `id: ...` / `"id": ...` args. +gts-id = { workspace = true } +gts = { workspace = true } diff --git a/libs/toolkit-gts-macros/src/lib.rs b/libs/toolkit-gts-macros/src/lib.rs index 0af6fecd9..ab14b2762 100644 --- a/libs/toolkit-gts-macros/src/lib.rs +++ b/libs/toolkit-gts-macros/src/lib.rs @@ -28,6 +28,58 @@ use syn::{Attribute, ExprStruct, Ident, ItemStruct, LitStr, parse_macro_input, p const TOOLKIT_GTS_PKG: &str = "cf-gears-toolkit-gts"; const TOOLKIT_GTS_LIB: &str = "toolkit_gts"; +/// The marker macro name recognised inside `type_id = ...` / `id: ...` / +/// `"id": ...` arguments. Mirrors upstream `gts_macros::gts_id`. When the +/// wrapper sees `gts_id!("")` it expands it to the full id literal +/// by prepending the compile-time `gts_id::GTS_ID_PREFIX`. +const PREFIX_MACRO: &str = "gts_id"; + +/// Build a full-id `LitStr` from a suffix written inside `gts_id!("")`, +/// preserving the suffix literal's span for diagnostics. +fn build_prefixed_lit(suffix: &LitStr) -> LitStr { + LitStr::new( + &format!("{}{}", gts_id::GTS_ID_PREFIX, suffix.value()), + suffix.span(), + ) +} + +/// Resolve a `gts_id` marker path (possibly qualified: `toolkit_gts::gts_id`, +/// `gts_macros::gts_id`, etc.) — returns `true` if it ends in `gts_id`. +fn is_prefix_macro_path(path: &syn::Path) -> bool { + path.segments + .last() + .is_some_and(|seg| seg.ident == PREFIX_MACRO) +} + +/// Extract a full-id `LitStr` from an expression that is either a plain +/// string literal or the `gts_id!("")` marker form. Mirrors +/// upstream's `gts_id_lit_from_expr` so the wrapper accepts exactly the +/// same input shapes as the underlying macros. +fn lit_from_id_expr(expr: &syn::Expr) -> syn::Result { + match expr { + syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(s), + .. + }) => Ok(s.clone()), + syn::Expr::Macro(syn::ExprMacro { mac, .. }) if is_prefix_macro_path(&mac.path) => { + let suffix: LitStr = mac.parse_body().map_err(|_| { + syn::Error::new_spanned( + mac, + format!( + "`{PREFIX_MACRO}!` takes a single string-literal suffix, \ + e.g. `{PREFIX_MACRO}!(\"x.core.events.topic.v1~\")`" + ), + ) + })?; + Ok(build_prefixed_lit(&suffix)) + } + other => Err(syn::Error::new_spanned( + other, + format!("expected a string literal or `{PREFIX_MACRO}!(\"...\")`"), + )), + } +} + /// Resolves the path to the `toolkit_gts` crate at the expansion site. /// /// Mirrors the `proc-macro-crate` dance used elsewhere in the workspace: @@ -80,10 +132,13 @@ fn instance_id_prefix(instance_id: &LitStr) -> LitStr { // #[gts_type_schema(...)] // ===================================================================== -/// Walk the attribute token stream and pull out the `type_id = "..."` -/// pair. Used to populate `InventoryTypeSchema::type_id` — the only piece -/// of information the wrapper needs from the attribute. Everything else -/// is forwarded verbatim and parsed by upstream. +/// Walk the attribute token stream and pull out the `type_id = "..."` (or +/// `type_id = gts_id!("...")`) pair. Used to populate +/// `InventoryTypeSchema::type_id` — the only piece of information the +/// wrapper needs from the attribute. Everything else is forwarded verbatim +/// and parsed by upstream. Both the literal and the `gts_id!("")` +/// marker forms are accepted; the marker form is expanded here to the +/// full id by prepending the compile-time `GTS_ID_PREFIX`. fn extract_type_id(attr: &TokenStream2) -> syn::Result { let mut iter = attr.clone().into_iter().peekable(); while let Some(tt) = iter.next() { @@ -96,14 +151,26 @@ fn extract_type_id(attr: &TokenStream2) -> syn::Result { if p.as_char() != '=' { return Err(syn::Error::new_spanned(&tt, "expected `=` after `type_id`")); } - let Some(TokenTree::Literal(lit)) = iter.next() else { - return Err(syn::Error::new_spanned( + // Collect tokens until the next top-level `,` — this is the + // value expression (a string literal or `gts_id!("...")`). + let mut value_tokens: TokenStream2 = TokenStream2::new(); + while let Some(nt) = iter.peek().cloned() { + match &nt { + TokenTree::Punct(p) if p.as_char() == ',' => break, + _ => { + value_tokens.extend([iter.next().unwrap()]); + } + } + } + let expr: syn::Expr = parse2(value_tokens).map_err(|e| { + syn::Error::new_spanned( &tt, - "`type_id = ...` must be a string literal", - )); - }; - let lit_ts: TokenStream2 = TokenTree::Literal(lit).into(); - return parse2::(lit_ts); + format!( + "`type_id = ...` must be a string literal or `{PREFIX_MACRO}!(\"...\")`: {e}" + ), + ) + })?; + return lit_from_id_expr(&expr); } } Err(syn::Error::new( @@ -124,7 +191,7 @@ fn extract_type_id(attr: &TokenStream2) -> syn::Result { /// ```ignore /// #[toolkit_gts::gts_type_schema( /// dir_path = "schemas", -/// type_id = "gts.cf.toolkit.plugins.plugin.v1~", +/// type_id = gts_id!("cf.toolkit.plugins.plugin.v1~"), /// description = "Base toolkit plugin schema", /// properties = "id,vendor,priority,properties", /// base = true, @@ -187,6 +254,140 @@ fn expand_gts_type_schema(attr: &TokenStream2, input: &ItemStruct) -> syn::Resul }) } +// ===================================================================== +// gts_id! — pass-through to upstream +// ===================================================================== + +/// Construct a full GTS identifier string from a suffix literal. +/// +/// If the literal does **not** already start with `GTS_ID_PREFIX`, the +/// prefix is prepended by delegating to upstream `gts_macros::gts_id!`. +/// If it **does** already start with `GTS_ID_PREFIX`, the literal is +/// emitted as-is — the prefix is not doubled. +/// +/// `gts_id!("x.core.events.topic.v1~")` expands to a `&'static str` +/// literal equal to +/// `concat!(GTS_ID_PREFIX, "x.core.events.topic.v1~")` — i.e. the +/// configured prefix (`gts.` by default, overridable via the +/// `GTS_ID_PREFIX` environment variable at compile time) followed by the +/// given suffix. +/// +/// The same `gts_id!("...")` form is also recognised as a marker inside +/// the `type_id`/`id` arguments of `#[gts_type_schema]`, +/// `toolkit_gts::gts_instance!`, and `toolkit_gts::gts_instance_raw!`, +/// so identifiers can be written prefix-free everywhere. +/// +/// ```ignore +/// use toolkit_gts::gts_id; +/// +/// let id: &str = gts_id!("acme.core.events.topic.v1~ven.app.x.v1"); +/// ``` +#[proc_macro] +pub fn gts_id(input: TokenStream) -> TokenStream { + let suffix = parse_macro_input!(input as LitStr); + let already_prefixed = suffix.value().starts_with(gts_id::GTS_ID_PREFIX); + match resolve_crate_path() { + Ok(crate_path) => { + if already_prefixed { + if let Err(e) = gts_id::GtsIdPattern::try_new(&suffix.value()) { + return syn::Error::new_spanned( + &suffix, + format!("gts_id!: invalid GTS ID pattern: {e}"), + ) + .to_compile_error() + .into(); + } + quote!(#suffix) + } else { + quote!(#crate_path::__private::upstream_gts_id!(#suffix)) + } + } + Err(e) => e.to_compile_error(), + } + .into() +} + +// ===================================================================== +// gts_uri! — URI-prefixed GTS IDs +// ===================================================================== + +/// Construct a GTS URI string from a GTS ID suffix literal, a literal that +/// already includes `GTS_ID_PREFIX` or `GTS_ID_URI_PREFIX`, or a runtime GTS +/// ID expression. +/// +/// `gts_uri!("x.core.events.topic.v1~")` expands to a `&'static str` +/// literal equal to `GTS_ID_URI_PREFIX + toolkit_gts::gts_id!(suffix)`. +/// With the default configuration that is +/// `"gts://gts.x.core.events.topic.v1~"`. +/// +/// If the literal already starts with `GTS_ID_URI_PREFIX` (e.g. +/// `"gts://gts.x.core.events.topic.v1~"`), it is emitted as-is. +/// +/// If the literal already starts with `GTS_ID_PREFIX` (e.g. +/// `"gts.x.core.events.topic.v1~"`), the prefix is not added again — only +/// `GTS_ID_URI_PREFIX` is prepended. +/// +/// The suffix is validated by [`gts_id!`], which delegates to upstream +/// `gts_macros::gts_id!` after applying the configured `GTS_ID_PREFIX`. +/// Non-literal expressions expand to a `String`: if the value already starts +/// with `GTS_ID_URI_PREFIX` it is returned as-is; otherwise +/// `GTS_ID_URI_PREFIX` is prepended, and `GTS_ID_PREFIX` is also inserted if +/// not already present. +/// +/// ```ignore +/// use toolkit_gts::gts_uri; +/// +/// let schema_uri: &str = gts_uri!("acme.core.events.topic.v1~"); +/// let runtime_uri: String = gts_uri!(schema_id); +/// ``` +#[proc_macro] +pub fn gts_uri(input: TokenStream) -> TokenStream { + let expr = parse_macro_input!(input as syn::Expr); + if let syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(suffix), + .. + }) = &expr + { + let already_uri = suffix.value().starts_with(gts::GTS_ID_URI_PREFIX); + return match resolve_crate_path() { + Ok(crate_path) => { + if already_uri { + let id_part = &suffix.value()[gts::GTS_ID_URI_PREFIX.len()..]; + if let Err(e) = gts_id::GtsIdPattern::try_new(id_part) { + return syn::Error::new_spanned( + suffix, + format!("gts_uri!: invalid GTS ID pattern: {e}"), + ) + .to_compile_error() + .into(); + } + quote!(#suffix) + } else { + let uri_prefix = LitStr::new(gts::GTS_ID_URI_PREFIX, suffix.span()); + quote!(::std::concat!(#uri_prefix, #crate_path::gts_id!(#suffix))) + } + } + Err(e) => e.to_compile_error(), + } + .into(); + } + + match resolve_crate_path() { + Ok(crate_path) => quote!({ + let __v = #expr; + if __v.starts_with(#crate_path::GTS_ID_URI_PREFIX) { + ::std::format!("{}", __v) + } else if __v.starts_with(#crate_path::GTS_ID_PREFIX) { + ::std::format!("{}{}", #crate_path::GTS_ID_URI_PREFIX, __v) + } else { + ::std::format!("{}{}{}", #crate_path::GTS_ID_URI_PREFIX, #crate_path::GTS_ID_PREFIX, __v) + } + }), + Err(e) => e.to_compile_error(), + } + .into() +} + // ===================================================================== // gts_instance! / gts_instance_raw! // ===================================================================== @@ -214,7 +415,7 @@ impl Parse for InstanceInput { let instance: ExprStruct = input.parse().map_err(|e| { syn::Error::new( e.span(), - "expected a struct literal: `StructPath { id: \"gts...\", ...other fields }`", + "expected a struct literal: `StructPath { id: gts_id!(\"...\"), ...other fields }`", ) })?; if !input.is_empty() { @@ -239,23 +440,14 @@ fn extract_id_literal(instance: &ExprStruct) -> syn::Result { if !ID_FIELD_NAMES.contains(&ident.to_string().as_str()) { continue; } - let syn::Expr::Lit(syn::ExprLit { - lit: syn::Lit::Str(lit_str), - .. - }) = &field.expr - else { - return Err(syn::Error::new_spanned( - &field.expr, - "GTS id field must be a string literal containing the full instance id (e.g. \"gts.acme.core.events.topic.v1~vendor.app.x.v1\")", - )); - }; + let lit_str = lit_from_id_expr(&field.expr)?; if found.is_some() { return Err(syn::Error::new_spanned( field, "ambiguous id field: only one of `id`, `gts_id`, `gtsId` may be set", )); } - found = Some(lit_str.clone()); + found = Some(lit_str); } found.ok_or_else(|| { syn::Error::new_spanned( @@ -273,7 +465,7 @@ fn extract_id_literal(instance: &ExprStruct) -> syn::Result { /// ```ignore /// toolkit_gts::gts_instance! { /// AuthzPermissionV1 { -/// id: "gts.cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_read.v1", +/// id: gts_id!("cf.toolkit.authz.permission.v1~cf.mini_chat._.chat_read.v1"), /// resource_type: "...".to_owned(), /// action: "read".to_owned(), /// display_name: "Read chat".to_owned(), @@ -286,7 +478,7 @@ fn extract_id_literal(instance: &ExprStruct) -> syn::Result { /// ```ignore /// toolkit_gts::gts_instance! { /// #[gts_static(CHAT_READ_PERM)] -/// AuthzPermissionV1 { id: "gts...", /* ... */ } +/// AuthzPermissionV1 { id: gts_id!("..."), /* ... */ } /// } /// /// let p: &AuthzPermissionV1 = &CHAT_READ_PERM; @@ -350,7 +542,7 @@ fn expand_gts_instance(input: TokenStream2) -> syn::Result { /// /// ```ignore /// toolkit_gts::gts_instance_raw!({ -/// "id": "gts.cf.core.events.topic.v1~cf.core._.audit.v1", +/// "id": gts_id!("cf.core.events.topic.v1~cf.core._.audit.v1"), /// "name": "audit", /// "description": "Audit log events", /// }); @@ -364,8 +556,10 @@ pub fn gts_instance_raw(input: TokenStream) -> TokenStream { } /// Walk a brace-delimited JSON object literal and locate the top-level -/// `"id"` key's string-literal value. Mirrors upstream's check; the -/// wrapper needs the value for the `InventoryInstance` fields. +/// `"id"` key's value, accepting either a plain string literal or the +/// `gts_id!("...")` marker form. The wrapper needs the value for the +/// `InventoryInstance` fields; the marker form is expanded to the full +/// id here by prepending the compile-time `GTS_ID_PREFIX`. fn extract_raw_id_literal(body: &TokenStream2) -> syn::Result { let mut iter = body.clone().into_iter().peekable(); while let Some(tt) = iter.next() { @@ -396,14 +590,27 @@ fn extract_raw_id_literal(body: &TokenStream2) -> syn::Result { "expected `:` after `\"id\"` key", )); } - let Some(TokenTree::Literal(value_lit)) = iter.next() else { - return Err(syn::Error::new_spanned( - tt, - "`\"id\"` must be a string literal containing the full GTS instance id", - )); - }; - let v_ts: TokenStream2 = TokenTree::Literal(value_lit).into(); - return parse2::(v_ts); + // Collect tokens until the next top-level `,` — this is the value + // expression (a string literal or `gts_id!("...")`). + let mut value_tokens: TokenStream2 = TokenStream2::new(); + while let Some(nt) = iter.peek().cloned() { + match &nt { + TokenTree::Punct(p) if p.as_char() == ',' => break, + _ => { + value_tokens.extend([iter.next().unwrap()]); + } + } + } + let expr: syn::Expr = parse2(value_tokens).map_err(|e| { + syn::Error::new_spanned( + &tt, + format!( + "`\"id\"` must be a string literal or `{PREFIX_MACRO}!(\"...\")` \ + containing the full GTS instance id: {e}" + ), + ) + })?; + return lit_from_id_expr(&expr); } Err(syn::Error::new( proc_macro2::Span::call_site(), diff --git a/libs/toolkit-gts/src/lib.rs b/libs/toolkit-gts/src/lib.rs index 1577b2630..0b74a3538 100644 --- a/libs/toolkit-gts/src/lib.rs +++ b/libs/toolkit-gts/src/lib.rs @@ -36,7 +36,7 @@ pub use plugin::PluginV1; // Re-export GTS primitives used by the wrapper macros and downstream // callers. Keeps `toolkit_gts::*` self-sufficient at the top level. -pub use gts::{GtsInstanceId, GtsSchema}; +pub use gts::{GTS_ID_PREFIX, GTS_ID_URI_PREFIX, GtsId, GtsInstanceId, GtsSchema}; // Re-export `inventory` so the macro expansions can emit // `::inventory::submit!` without requiring consumer crates to add @@ -45,7 +45,8 @@ pub use gts::{GtsInstanceId, GtsSchema}; pub use inventory; // Re-export the companion proc-macros so consumers need only one crate dep. -pub use toolkit_gts_macros::{gts_instance, gts_instance_raw, gts_type_schema}; +pub use gts_macros::GtsTraitsSchema; +pub use toolkit_gts_macros::{gts_id, gts_instance, gts_instance_raw, gts_type_schema, gts_uri}; /// Hidden re-exports used by the `cf-gears-toolkit-gts-macros` proc-macro /// expansions to reach the upstream construction macros without forcing @@ -53,7 +54,8 @@ pub use toolkit_gts_macros::{gts_instance, gts_instance_raw, gts_type_schema}; #[doc(hidden)] pub mod __private { pub use ::gts_macros::{ - gts_instance as upstream_gts_instance, gts_instance_raw as upstream_gts_instance_raw, + gts_id as upstream_gts_id, gts_instance as upstream_gts_instance, + gts_instance_raw as upstream_gts_instance_raw, struct_to_gts_schema as upstream_struct_to_gts_schema, }; } @@ -168,11 +170,11 @@ mod tests { .map(|e| e.type_id) .collect(); assert!( - ids.contains(&"gts.cf.toolkit.plugins.plugin.v1~"), + ids.contains(&crate::gts_id!("cf.toolkit.plugins.plugin.v1~")), "PluginV1 not registered; got ids: {ids:?}" ); assert!( - ids.contains(&"gts.cf.toolkit.authz.permission.v1~"), + ids.contains(&crate::gts_id!("cf.toolkit.authz.permission.v1~")), "AuthzPermissionV1 not registered; got ids: {ids:?}" ); assert_eq!( diff --git a/libs/toolkit-gts/src/permission.rs b/libs/toolkit-gts/src/permission.rs index 034f71ea8..b70658b92 100644 --- a/libs/toolkit-gts/src/permission.rs +++ b/libs/toolkit-gts/src/permission.rs @@ -53,7 +53,7 @@ use gts::GtsInstanceId; /// GTS Type Identifier: `gts.cf.toolkit.authz.permission.v1~` #[gts_type_schema( dir_path = "schemas", - type_id = "gts.cf.toolkit.authz.permission.v1~", + type_id = gts_id!("cf.toolkit.authz.permission.v1~"), description = "Gears authorization permission", properties = "id,resource_type,action,display_name", base = true diff --git a/libs/toolkit-gts/src/plugin.rs b/libs/toolkit-gts/src/plugin.rs index b97b26370..275583ca0 100644 --- a/libs/toolkit-gts/src/plugin.rs +++ b/libs/toolkit-gts/src/plugin.rs @@ -29,7 +29,7 @@ use gts::GtsInstanceId; #[derive(Debug)] #[gts_type_schema( dir_path = "schemas", - type_id = "gts.cf.toolkit.plugins.plugin.v1~", + type_id = gts_id!("cf.toolkit.plugins.plugin.v1~"), description = "Base toolkit plugin schema", properties = "id,vendor,priority,properties", base = true diff --git a/libs/toolkit-gts/tests/macro_integration.rs b/libs/toolkit-gts/tests/macro_integration.rs index 9d5d92df4..0c6208b5a 100644 --- a/libs/toolkit-gts/tests/macro_integration.rs +++ b/libs/toolkit-gts/tests/macro_integration.rs @@ -9,11 +9,10 @@ //! emission) is upstream's contract and is covered by upstream's own //! tests. -use gts_macros::GtsTraitsSchema; use schemars::JsonSchema; use toolkit_gts::{ - GtsInstanceId, GtsSchema, InventoryInstance, InventoryTypeSchema, gts_instance, - gts_instance_raw, gts_type_schema, + GtsInstanceId, GtsSchema, GtsTraitsSchema, InventoryInstance, InventoryTypeSchema, gts_id, + gts_instance, gts_instance_raw, gts_type_schema, gts_uri, }; // ===================================================================== @@ -26,7 +25,7 @@ use toolkit_gts::{ /// 2. Submit an `InventoryTypeSchema` entry with the same `type_id`. #[gts_type_schema( dir_path = "schemas", - type_id = "gts.test.cf.toolkit_gts.thing.v1~", + type_id = gts_id!("test.cf.toolkit_gts.thing.v1~"), description = "Test base type for toolkit-gts wrapper integration tests", properties = "id,name", base = true @@ -39,14 +38,14 @@ pub struct TestThingV1 { // `gts_instance_raw!` — submits one inventory entry; the value itself // is built lazily by the closure inside `payload_fn`. gts_instance_raw!({ - "id": "gts.test.cf.toolkit_gts.thing.v1~test.cf.toolkit_gts.raw.v1", + "id": gts_id!("test.cf.toolkit_gts.thing.v1~test.cf.toolkit_gts.raw.v1"), "name": "raw", }); // `gts_instance!` (typed) — same: one inventory entry, value built lazily. gts_instance! { TestThingV1 { - id: "gts.test.cf.toolkit_gts.thing.v1~test.cf.toolkit_gts.typed.v1", + id: gts_id!("test.cf.toolkit_gts.thing.v1~test.cf.toolkit_gts.typed.v1"), name: "typed".to_owned(), } } @@ -58,7 +57,7 @@ gts_instance! { gts_instance! { #[gts_static(NAMED_INSTANCE)] TestThingV1 { - id: "gts.test.cf.toolkit_gts.thing.v1~test.cf.toolkit_gts.named.v1", + id: gts_id!("test.cf.toolkit_gts.thing.v1~test.cf.toolkit_gts.named.v1"), name: "named".to_owned(), } } @@ -76,7 +75,7 @@ gts_instance! { #[derive(JsonSchema, serde::Serialize, GtsTraitsSchema)] pub struct EventTraits { - #[schemars(extend("x-gts-ref" = "gts.x.core.events.topic.v1~"))] + #[schemars(extend("x-gts-ref" = gts_id!("x.core.events.topic.v1~")))] pub topic_ref: String, } @@ -86,7 +85,7 @@ pub struct EventTraits { #[gts_type_schema( dir_path = "schemas", base = true, - type_id = "gts.test.cf.toolkit_gts.event.v1~", + type_id = gts_id!("test.cf.toolkit_gts.event.v1~"), description = "Abstract base event with inline traits schema", properties = "id,payload", traits_schema = inline(EventTraits), @@ -102,10 +101,10 @@ pub struct EventV1

{ #[gts_type_schema( dir_path = "schemas", base = EventV1, - type_id = "gts.test.cf.toolkit_gts.event.v1~test.cf.toolkit_gts.order_placed.v1~", + type_id = gts_id!("test.cf.toolkit_gts.event.v1~test.cf.toolkit_gts.order_placed.v1~"), description = "Final order-placed event", properties = "order_id", - traits = serde_json::json!({ "topic_ref": "gts.x.core.events.topic.v1~test.cf._.orders.v1" }), + traits = serde_json::json!({ "topic_ref": gts_id!("x.core.events.topic.v1~test.cf._.orders.v1") }), gts_final = true )] pub struct OrderPlacedV1 { @@ -116,13 +115,14 @@ pub struct OrderPlacedV1 { // Tests // ===================================================================== -const TYPE_ID: &str = "gts.test.cf.toolkit_gts.thing.v1~"; -const EVENT_BASE_ID: &str = "gts.test.cf.toolkit_gts.event.v1~"; +const TYPE_ID: &str = gts_id!("test.cf.toolkit_gts.thing.v1~"); +const EVENT_BASE_ID: &str = gts_id!("test.cf.toolkit_gts.event.v1~"); const ORDER_PLACED_ID: &str = - "gts.test.cf.toolkit_gts.event.v1~test.cf.toolkit_gts.order_placed.v1~"; -const RAW_ID: &str = "gts.test.cf.toolkit_gts.thing.v1~test.cf.toolkit_gts.raw.v1"; -const TYPED_ID: &str = "gts.test.cf.toolkit_gts.thing.v1~test.cf.toolkit_gts.typed.v1"; -const NAMED_ID: &str = "gts.test.cf.toolkit_gts.thing.v1~test.cf.toolkit_gts.named.v1"; + gts_id!("test.cf.toolkit_gts.event.v1~test.cf.toolkit_gts.order_placed.v1~"); +const RAW_ID: &str = gts_id!("test.cf.toolkit_gts.thing.v1~test.cf.toolkit_gts.raw.v1"); +const TYPED_ID: &str = gts_id!("test.cf.toolkit_gts.thing.v1~test.cf.toolkit_gts.typed.v1"); +const NAMED_ID: &str = gts_id!("test.cf.toolkit_gts.thing.v1~test.cf.toolkit_gts.named.v1"); +const TYPE_URI: &str = gts_uri!("test.cf.toolkit_gts.thing.v1~"); fn schema_ids() -> Vec<&'static str> { inventory::iter:: @@ -145,6 +145,64 @@ fn find_instance(id: &str) -> &'static InventoryInstance { .unwrap_or_else(|| panic!("instance {id} not in inventory; got: {:?}", instance_ids())) } +#[test] +fn gts_uri_macro_prepends_uri_and_configured_id_prefix() { + assert_eq!(TYPE_URI, gts_uri!("test.cf.toolkit_gts.thing.v1~")); + assert!(TYPE_URI.ends_with(TYPE_ID)); +} + +#[test] +fn gts_id_macro_detects_already_prefixed_literal() { + // gts_id! on a literal that already starts with GTS_ID_PREFIX should + // emit it as-is, without doubling the prefix. + let already_prefixed: &str = gts_id!("gts.test.cf.toolkit_gts.thing.v1~"); + assert_eq!(already_prefixed, TYPE_ID); +} + +#[test] +fn gts_uri_macro_detects_already_prefixed_literal() { + // A literal that already includes GTS_ID_PREFIX should not get it doubled. + let already_prefixed: &str = concat!(gts_id!("test.cf.toolkit_gts.thing.v1~")); + let uri_from_prefixed: String = gts_uri!(already_prefixed); + let uri_from_suffix: &str = gts_uri!("test.cf.toolkit_gts.thing.v1~"); + assert_eq!(uri_from_prefixed.as_str(), uri_from_suffix); +} + +#[test] +fn gts_uri_macro_detects_already_uri_prefixed_literal() { + // A literal that already starts with GTS_ID_URI_PREFIX should be + // emitted as-is. + let already_uri: &str = gts_uri!("gts://gts.test.cf.toolkit_gts.thing.v1~"); + assert_eq!(already_uri, TYPE_URI); +} + +#[test] +fn gts_uri_macro_runtime_expr_with_uri_prefix() { + // Runtime expression that already starts with GTS_ID_URI_PREFIX — + // returned as-is. + let uri_in: String = TYPE_URI.to_owned(); + let uri: String = gts_uri!(uri_in); + assert_eq!(uri.as_str(), TYPE_URI); +} + +#[test] +fn gts_uri_macro_runtime_expr_with_prefix() { + // Runtime expression that already starts with GTS_ID_PREFIX — only URI + // prefix should be prepended. + let id: String = TYPE_ID.to_owned(); + let uri: String = gts_uri!(id); + assert_eq!(uri.as_str(), TYPE_URI); +} + +#[test] +fn gts_uri_macro_runtime_expr_without_prefix() { + // Runtime expression without GTS_ID_PREFIX — both URI and ID prefix + // should be prepended. + let suffix: String = "test.cf.toolkit_gts.thing.v1~".to_owned(); + let uri: String = gts_uri!(suffix); + assert_eq!(uri.as_str(), TYPE_URI); +} + #[test] fn gts_type_schema_wrapper_registers_inventory_schema() { // Wrapper contract: `#[gts_type_schema(...)]` adds an `InventoryTypeSchema` diff --git a/libs/toolkit-gts/tests/prefix_customization.rs b/libs/toolkit-gts/tests/prefix_customization.rs new file mode 100644 index 000000000..11d2b38cc --- /dev/null +++ b/libs/toolkit-gts/tests/prefix_customization.rs @@ -0,0 +1,154 @@ +//! Tests that verify the `GTS_ID_PREFIX` compile-time customization works +//! end-to-end through the `gts_id!` and `gts_uri!` macros. +//! +//! Two layers of tests: +//! +//! 1. **In-process** — structural invariants that pass with any valid prefix +//! (default or custom). These verify the macros produce correct results +//! for whatever prefix was compiled in. +//! 2. **Subprocess** — spawn `cargo test` with different `GTS_ID_PREFIX` env +//! vars and verify the compiled-in prefix changes accordingly. Since the +//! prefix is a compile-time constant (`option_env!`), changing it requires +//! a rebuild, which the subprocess triggers. A guard env var +//! (`GTS_PREFIX_TEST_SPAWNED`) prevents infinite recursion. + +use std::process::Command; +use toolkit_gts::{GTS_ID_PREFIX, GTS_ID_URI_PREFIX, gts_id, gts_uri}; + +const SUFFIX: &str = "test.cf.toolkit_gts.prefix_check.v1~"; +const TYPE_ID: &str = gts_id!("test.cf.toolkit_gts.prefix_check.v1~"); +const TYPE_URI: &str = gts_uri!("test.cf.toolkit_gts.prefix_check.v1~"); + +// ===================================================================== +// Layer 1: in-process structural invariants (pass with any prefix) +// ===================================================================== + +#[test] +fn gts_id_macro_prepends_configured_prefix() { + assert!( + TYPE_ID.starts_with(GTS_ID_PREFIX), + "gts_id! result \"{TYPE_ID}\" does not start with GTS_ID_PREFIX \"{GTS_ID_PREFIX}\"" + ); + assert!( + TYPE_ID.ends_with(SUFFIX), + "gts_id! result \"{TYPE_ID}\" does not end with the suffix \"{SUFFIX}\"" + ); + assert_eq!(TYPE_ID, format!("{GTS_ID_PREFIX}{SUFFIX}")); +} + +#[test] +fn gts_uri_macro_prepends_uri_and_configured_id_prefix() { + assert!( + TYPE_URI.starts_with(GTS_ID_URI_PREFIX), + "gts_uri! result \"{TYPE_URI}\" does not start with GTS_ID_URI_PREFIX \"{GTS_ID_URI_PREFIX}\"" + ); + let expected = format!("{GTS_ID_URI_PREFIX}{GTS_ID_PREFIX}{SUFFIX}"); + assert_eq!(TYPE_URI, expected); +} + +#[test] +fn prefix_constant_is_well_formed() { + println!("Compiled with GTS_ID_PREFIX = \"{GTS_ID_PREFIX}\""); + assert!(!GTS_ID_PREFIX.is_empty()); + assert!(GTS_ID_PREFIX.ends_with('.')); +} + +// ===================================================================== +// Layer 2: subprocess tests — verify env var actually changes the prefix +// ===================================================================== + +/// Guard env var — when set, subprocess-spawning tests skip themselves to +/// avoid infinite recursion (the spawned `cargo test` would re-run them). +const SPAWNED_GUARD: &str = "GTS_PREFIX_TEST_SPAWNED"; + +/// Run `cargo test --test prefix_customization prefix_constant_is_well_formed +/// -- --nocapture` with the given `GTS_ID_PREFIX` env var and return the +/// combined stdout+stderr output. +fn run_with_prefix(prefix: Option<&str>) -> (bool, String) { + let mut cmd = Command::new("cargo"); + cmd.args([ + "test", + "-p", + "cf-gears-toolkit-gts", + "--test", + "prefix_customization", + "prefix_constant_is_well_formed", + "--", + "--nocapture", + ]); + if let Some(p) = prefix { + cmd.env("GTS_ID_PREFIX", p); + } + cmd.env(SPAWNED_GUARD, "1"); + + let output = match cmd.output() { + Ok(o) => o, + Err(e) => panic!("failed to spawn cargo: {e}"), + }; + let combined = format!( + "{}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + (output.status.success(), combined) +} + +#[test] +fn default_prefix_is_gts_dot() { + if std::env::var(SPAWNED_GUARD).is_ok() { + return; + } + + let (ok, output) = run_with_prefix(None); + assert!(ok, "cargo test failed:\n{output}"); + assert!( + output.contains(r#"Compiled with GTS_ID_PREFIX = "gts.""#), + "expected default prefix \"gts.\" in output, got:\n{output}" + ); +} + +#[test] +fn custom_prefix_acme_dot() { + if std::env::var(SPAWNED_GUARD).is_ok() { + return; + } + + let (ok, output) = run_with_prefix(Some("acme.")); + assert!(ok, "cargo test failed:\n{output}"); + assert!( + output.contains(r#"Compiled with GTS_ID_PREFIX = "acme.""#), + "expected custom prefix \"acme.\" in output, got:\n{output}" + ); +} + +#[test] +fn custom_prefix_myco_dot() { + if std::env::var(SPAWNED_GUARD).is_ok() { + return; + } + + let (ok, output) = run_with_prefix(Some("myco.")); + assert!(ok, "cargo test failed:\n{output}"); + assert!( + output.contains(r#"Compiled with GTS_ID_PREFIX = "myco.""#), + "expected custom prefix \"myco.\" in output, got:\n{output}" + ); +} + +#[test] +fn invalid_prefix_fails_to_compile() { + if std::env::var(SPAWNED_GUARD).is_ok() { + return; + } + + // "Acme." is invalid — uppercase is rejected by the const validator. + let (ok, output) = run_with_prefix(Some("Acme.")); + assert!( + !ok, + "expected compilation to fail with invalid prefix 'Acme.', but it succeeded:\n{output}" + ); + assert!( + output.contains("GTS_ID_PREFIX") || output.contains("panic"), + "expected error mentioning GTS_ID_PREFIX or panic, got:\n{output}" + ); +} diff --git a/libs/toolkit-odata/Cargo.toml b/libs/toolkit-odata/Cargo.toml index 7eab22f9b..50763c3e0 100644 --- a/libs/toolkit-odata/Cargo.toml +++ b/libs/toolkit-odata/Cargo.toml @@ -23,6 +23,7 @@ with-utoipa = ["dep:utoipa"] [dependencies] toolkit-canonical-errors = { workspace = true } +toolkit-gts = { workspace = true } bigdecimal = { workspace = true } uuid = { workspace = true } chrono = { workspace = true, default-features = false, features = ["clock"] } diff --git a/libs/toolkit-odata/src/errors.rs b/libs/toolkit-odata/src/errors.rs index 3b1e441af..639de45b0 100644 --- a/libs/toolkit-odata/src/errors.rs +++ b/libs/toolkit-odata/src/errors.rs @@ -2,5 +2,5 @@ use toolkit_canonical_errors::resource_error; -#[resource_error("gts.cf.core.odata.query.v1~")] +#[resource_error(gts_id!("cf.core.odata.query.v1~"))] pub struct OdataError; diff --git a/libs/toolkit-odata/src/problem_mapping.rs b/libs/toolkit-odata/src/problem_mapping.rs index d582d5b37..f120aa170 100644 --- a/libs/toolkit-odata/src/problem_mapping.rs +++ b/libs/toolkit-odata/src/problem_mapping.rs @@ -141,8 +141,9 @@ impl From for CanonicalError { mod tests { use super::*; use toolkit_canonical_errors::Problem; + use toolkit_gts::gts_id; - const ODATA_RESOURCE_TYPE: &str = "gts.cf.core.odata.query.v1~"; + const ODATA_RESOURCE_TYPE: &str = gts_id!("cf.core.odata.query.v1~"); fn wire(err: Error) -> Problem { Problem::from(CanonicalError::from(err)) diff --git a/libs/toolkit/src/api/operation_builder.rs b/libs/toolkit/src/api/operation_builder.rs index 54f891fe8..9cd214018 100644 --- a/libs/toolkit/src/api/operation_builder.rs +++ b/libs/toolkit/src/api/operation_builder.rs @@ -19,6 +19,7 @@ use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use std::marker::PhantomData; use toolkit_canonical_errors::problem; +use toolkit_gts::gts_id; /// Convert OpenAPI-style path placeholders to Axum 0.8+ style path parameters. /// @@ -61,6 +62,10 @@ pub fn axum_to_openapi_path(path: &str) -> String { path.replace("{*", "{") } +/// Canonical base license feature used by the example gears. +pub const CORE_GLOBAL_BASE_LICENSE_FEATURE: &str = + gts_id!("cf.core.lic.feat.v1~cf.core.global.base.v1"); + /// Type-state markers for compile-time enforcement pub mod state { /// Marker for missing required components @@ -900,7 +905,7 @@ where /// impl AsRef for License { /// fn as_ref(&self) -> &str { /// match self { - /// License::Base => "gts.cf.core.lic.feat.v1~cf.core.global.base.v1", + /// License::Base => CORE_GLOBAL_BASE_LICENSE_FEATURE, /// } /// } /// } diff --git a/libs/toolkit/src/client_hub.rs b/libs/toolkit/src/client_hub.rs index c13e21bf8..54a8f26ec 100644 --- a/libs/toolkit/src/client_hub.rs +++ b/libs/toolkit/src/client_hub.rs @@ -279,6 +279,7 @@ impl ClientHub { #[cfg_attr(coverage_nightly, coverage(off))] mod tests { use super::*; + use toolkit_gts::gts_id; #[async_trait::async_trait] trait TestApi: Send + Sync { @@ -337,12 +338,12 @@ mod tests { #[tokio::test] async fn scoped_register_and_get_dyn_trait() { let hub = ClientHub::new(); - let scope_a = ClientScope::gts_id( - "gts.cf.core.toolkit.plugins.v1~cf.core.tenant_resolver.plugin.v1~contoso.app._.plugin.v1.0", - ); - let scope_b = ClientScope::gts_id( - "gts.cf.core.toolkit.plugins.v1~cf.core.tenant_resolver.plugin.v1~fabrikam.app._.plugin.v1.0", - ); + let scope_a = ClientScope::gts_id(gts_id!( + "cf.core.toolkit.plugins.v1~cf.core.tenant_resolver.plugin.v1~contoso.app._.plugin.v1.0" + )); + let scope_b = ClientScope::gts_id(gts_id!( + "cf.core.toolkit.plugins.v1~cf.core.tenant_resolver.plugin.v1~fabrikam.app._.plugin.v1.0" + )); let api_a: Arc = Arc::new(ImplA(1)); let api_b: Arc = Arc::new(ImplA(2)); @@ -363,9 +364,9 @@ mod tests { #[test] fn scoped_get_is_independent_from_global_get() { let hub = ClientHub::new(); - let scope = ClientScope::gts_id( - "gts.cf.core.toolkit.plugins.v1~cf.core.tenant_resolver.plugin.v1~fabrikam.app._.plugin.v1.0", - ); + let scope = ClientScope::gts_id(gts_id!( + "cf.core.toolkit.plugins.v1~cf.core.tenant_resolver.plugin.v1~fabrikam.app._.plugin.v1.0" + )); hub.register::(Arc::from("global")); hub.register_scoped::(scope.clone(), Arc::from("scoped")); @@ -376,9 +377,9 @@ mod tests { #[test] fn try_get_scoped_returns_some_on_hit() { let hub = ClientHub::new(); - let scope = ClientScope::gts_id( - "gts.cf.core.toolkit.plugins.v1~cf.core.tenant_resolver.plugin.v1~contoso.app._.plugin.v1.0", - ); + let scope = ClientScope::gts_id(gts_id!( + "cf.core.toolkit.plugins.v1~cf.core.tenant_resolver.plugin.v1~contoso.app._.plugin.v1.0" + )); hub.register_scoped::(scope.clone(), Arc::from("scoped")); let got = hub.try_get_scoped::(&scope); @@ -388,9 +389,9 @@ mod tests { #[test] fn try_get_scoped_returns_none_on_miss() { let hub = ClientHub::new(); - let scope = ClientScope::gts_id( - "gts.cf.core.toolkit.plugins.v1~cf.core.tenant_resolver.plugin.v1~fabrikam.app._.plugin.v1.0", - ); + let scope = ClientScope::gts_id(gts_id!( + "cf.core.toolkit.plugins.v1~cf.core.tenant_resolver.plugin.v1~fabrikam.app._.plugin.v1.0" + )); let got = hub.try_get_scoped::(&scope); assert!(got.is_none()); diff --git a/libs/toolkit/src/plugins/mod.rs b/libs/toolkit/src/plugins/mod.rs index be6075637..d061a6c87 100644 --- a/libs/toolkit/src/plugins/mod.rs +++ b/libs/toolkit/src/plugins/mod.rs @@ -210,6 +210,15 @@ mod tests { use super::*; use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; + use toolkit_gts::gts_id; + + const PLUGIN_A: &str = + gts_id!("cf.toolkit.plugins.plugin.v1~cf.core.test.plugin.v1~vendor.a.test.plugin.v1"); + const PLUGIN_B: &str = + gts_id!("cf.toolkit.plugins.plugin.v1~cf.core.test.plugin.v1~vendor.b.test.plugin.v1"); + const CONCURRENT_PLUGIN: &str = gts_id!( + "cf.toolkit.plugins.plugin.v1~cf.core.test.plugin.v1~vendor.concurrent.test.plugin.v1" + ); #[tokio::test] async fn resolve_called_once_returns_same_str() { @@ -220,10 +229,7 @@ mod tests { let id_a = selector .get_or_init(|| async move { calls_a.fetch_add(1, Ordering::SeqCst); - Ok::<_, std::convert::Infallible>( - "gts.cf.toolkit.plugins.plugin.v1~cf.core.test.plugin.v1~vendor.a.test.plugin.v1" - .to_owned(), - ) + Ok::<_, std::convert::Infallible>(PLUGIN_A.to_owned()) }) .await .unwrap(); @@ -232,10 +238,7 @@ mod tests { let id_b = selector .get_or_init(|| async move { calls_b.fetch_add(1, Ordering::SeqCst); - Ok::<_, std::convert::Infallible>( - "gts.cf.toolkit.plugins.plugin.v1~cf.core.test.plugin.v1~vendor.b.test.plugin.v1" - .to_owned(), - ) + Ok::<_, std::convert::Infallible>(PLUGIN_B.to_owned()) }) .await .unwrap(); @@ -253,16 +256,10 @@ mod tests { let id_a = selector .get_or_init(|| async move { calls_a.fetch_add(1, Ordering::SeqCst); - Ok::<_, std::convert::Infallible>( - "gts.cf.toolkit.plugins.plugin.v1~cf.core.test.plugin.v1~vendor.a.test.plugin.v1" - .to_owned(), - ) + Ok::<_, std::convert::Infallible>(PLUGIN_A.to_owned()) }) .await; - assert_eq!( - &*id_a.unwrap(), - "gts.cf.toolkit.plugins.plugin.v1~cf.core.test.plugin.v1~vendor.a.test.plugin.v1" - ); + assert_eq!(&*id_a.unwrap(), PLUGIN_A); assert_eq!(calls.load(Ordering::SeqCst), 1); assert!(selector.reset().await); @@ -270,16 +267,10 @@ mod tests { let id_b = selector .get_or_init(|| async move { calls_b.fetch_add(1, Ordering::SeqCst); - Ok::<_, std::convert::Infallible>( - "gts.cf.toolkit.plugins.plugin.v1~cf.core.test.plugin.v1~vendor.b.test.plugin.v1" - .to_owned(), - ) + Ok::<_, std::convert::Infallible>(PLUGIN_B.to_owned()) }) .await; - assert_eq!( - &*id_b.unwrap(), - "gts.cf.toolkit.plugins.plugin.v1~cf.core.test.plugin.v1~vendor.b.test.plugin.v1" - ); + assert_eq!(&*id_b.unwrap(), PLUGIN_B); assert_eq!(calls.load(Ordering::SeqCst), 2); } @@ -298,10 +289,7 @@ mod tests { // Small delay to increase chance of concurrent access tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; calls.fetch_add(1, Ordering::SeqCst); - Ok::<_, std::convert::Infallible>( - "gts.cf.toolkit.plugins.plugin.v1~cf.core.test.plugin.v1~vendor.concurrent.test.plugin.v1" - .to_owned(), - ) + Ok::<_, std::convert::Infallible>(CONCURRENT_PLUGIN.to_owned()) }) .await })); @@ -315,10 +303,7 @@ mod tests { // All results should be the same for id in &results { - assert_eq!( - &**id, - "gts.cf.toolkit.plugins.plugin.v1~cf.core.test.plugin.v1~vendor.concurrent.test.plugin.v1" - ); + assert_eq!(&**id, CONCURRENT_PLUGIN); } // Resolve should have been called exactly once diff --git a/libs/toolkit/tests/integration_test.rs b/libs/toolkit/tests/integration_test.rs index ca267d5cb..a97241afe 100644 --- a/libs/toolkit/tests/integration_test.rs +++ b/libs/toolkit/tests/integration_test.rs @@ -10,7 +10,7 @@ use serde_json::Value; use std::sync::Mutex; use toolkit::api::{ Missing, OpenApiRegistry, OperationBuilder, OperationSpec, ParamLocation, - operation_builder::LicenseFeature, + operation_builder::{CORE_GLOBAL_BASE_LICENSE_FEATURE, LicenseFeature}, }; #[allow(dead_code)] @@ -21,7 +21,7 @@ enum TestLicenseFeatures { impl AsRef for TestLicenseFeatures { fn as_ref(&self) -> &'static str { match self { - TestLicenseFeatures::Base => "gts.cf.core.lic.feat.v1~cf.core.global.base.v1", + TestLicenseFeatures::Base => CORE_GLOBAL_BASE_LICENSE_FEATURE, } } } diff --git a/testing/e2e/gears/account_management/test_integration_seams.py b/testing/e2e/gears/account_management/test_integration_seams.py index 355fd09e7..b3c53eaf5 100644 --- a/testing/e2e/gears/account_management/test_integration_seams.py +++ b/testing/e2e/gears/account_management/test_integration_seams.py @@ -64,6 +64,9 @@ unique_name, ) +GTS_ID_PREFIX = "gts" + "." +GTS_URI_PREFIX = "gts://" + GTS_ID_PREFIX + # ── S1: Route smoke ───────────────────────────────────────────────────── @@ -90,7 +93,7 @@ def _route_reached(r: httpx.Response) -> bool: return False return isinstance(body, dict) and isinstance(body.get("type"), str) and body[ "type" - ].startswith("gts://gts.cf.core.errors.err.v1~") + ].startswith(f"{GTS_URI_PREFIX}cf.core.errors.err.v1~") @pytest.mark.smoke diff --git a/testing/e2e/gears/oagw/test_authz.py b/testing/e2e/gears/oagw/test_authz.py index cfea56e19..b68ff71b3 100644 --- a/testing/e2e/gears/oagw/test_authz.py +++ b/testing/e2e/gears/oagw/test_authz.py @@ -6,6 +6,9 @@ from .helpers import create_route, create_upstream, delete_upstream, unique_alias +GTS_ID_PREFIX = "gts" + "." +GTS_URI_PREFIX = "gts://" + GTS_ID_PREFIX + @pytest.mark.asyncio async def test_proxy_authz_allowed( @@ -82,6 +85,9 @@ async def test_proxy_authz_forbidden_nil_tenant( body = resp.json() assert body["status"] == 403 assert body["title"] == "Permission Denied" - assert body["type"] == "gts://gts.cf.core.errors.err.v1~cf.core.err.permission_denied.v1~" + assert ( + body["type"] + == f"{GTS_URI_PREFIX}cf.core.errors.err.v1~cf.core.err.permission_denied.v1~" + ) finally: await delete_upstream(client, oagw_base_url, oagw_headers, uid) diff --git a/testing/e2e/gears/types_registry/test_types_registry_list.py b/testing/e2e/gears/types_registry/test_types_registry_list.py index 348eabc4d..7fa80f95e 100644 --- a/testing/e2e/gears/types_registry/test_types_registry_list.py +++ b/testing/e2e/gears/types_registry/test_types_registry_list.py @@ -2,27 +2,30 @@ import httpx import pytest +GTS_ID_PREFIX = "gts" + "." +GTS_URI_PREFIX = "gts://" + GTS_ID_PREFIX + async def register_test_entities(client, base_url, auth_headers): """Helper to register test entities for list tests.""" payload = { "entities": [ { - "$id": "gts://gts.e2e.list.acme.models.user.v1~", + "$id": f"{GTS_URI_PREFIX}e2e.list.acme.models.user.v1~", "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": {"name": {"type": "string"}}, "description": "User type from acme vendor" }, { - "$id": "gts://gts.e2e.list.acme.events.created.v1~", + "$id": f"{GTS_URI_PREFIX}e2e.list.acme.events.created.v1~", "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": {"timestamp": {"type": "string"}}, "description": "Created event from acme vendor" }, { - "$id": "gts://gts.e2e.list.globex.models.product.v1~", + "$id": f"{GTS_URI_PREFIX}e2e.list.globex.models.product.v1~", "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": {"productId": {"type": "string"}}, @@ -402,7 +405,7 @@ async def test_list_entities_response_structure(base_url, auth_headers): payload = { "entities": [ { - "$id": "gts://gts.e2e.structure.models.test.v1~", + "$id": f"{GTS_URI_PREFIX}e2e.structure.models.test.v1~", "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": {"value": {"type": "string"}}, diff --git a/tools/dylint_lints/Cargo.lock b/tools/dylint_lints/Cargo.lock index cce238ef3..6df0edc6d 100644 --- a/tools/dylint_lints/Cargo.lock +++ b/tools/dylint_lints/Cargo.lock @@ -601,6 +601,7 @@ version = "0.7.4" dependencies = [ "axum 0.8.9", "cf-gears-toolkit-canonical-errors-macro", + "cf-gears-toolkit-gts", "http", "serde", "serde_json", @@ -613,6 +614,7 @@ dependencies = [ name = "cf-gears-toolkit-canonical-errors-macro" version = "0.6.1" dependencies = [ + "gts-id", "proc-macro-crate", "proc-macro2", "quote", @@ -678,6 +680,8 @@ dependencies = [ name = "cf-gears-toolkit-gts-macros" version = "0.1.2" dependencies = [ + "gts", + "gts-id", "proc-macro-crate", "proc-macro2", "quote", @@ -702,6 +706,7 @@ dependencies = [ "base64", "bigdecimal", "cf-gears-toolkit-canonical-errors", + "cf-gears-toolkit-gts", "chrono", "chrono-tz", "peg", @@ -1206,6 +1211,7 @@ dependencies = [ name = "de0901_gts_string_pattern" version = "0.1.0" dependencies = [ + "cf-gears-toolkit-gts", "clippy_utils", "dylint_linting", "dylint_testing", @@ -1895,9 +1901,9 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "gts" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78d5445277d7f0df598295f7e53608540ae5fe0b830d5010f8f7cce21c69d09f" +checksum = "04c85c9d9cd01c81990a8ccb5f9d584d83571f2d4df2ae6d974170c780bdf693" dependencies = [ "gts-id", "jsonschema", @@ -1908,24 +1914,24 @@ dependencies = [ "shellexpand", "thiserror 2.0.18", "tracing", - "uuid", "walkdir", ] [[package]] name = "gts-id" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a85a48bfcfaa558f7eaee74e9a7d153d0924eac2d4d287f93323b756fe2700fe" +checksum = "bb0e34e1934f1197b1cb68eb0020cb5aadd79a93b7b8f030d9ebee8dc5a027f2" dependencies = [ "thiserror 2.0.18", + "uuid", ] [[package]] name = "gts-macros" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20a84d8b5e87ab37b5539bb5e9d830b40235918f375405569c219f4de9b3f7d4" +checksum = "07961d01ac64b0ec00940d53a1361926cf759df8750fbaf236d43990e6718efe" dependencies = [ "gts-id", "proc-macro2", diff --git a/tools/dylint_lints/Cargo.toml b/tools/dylint_lints/Cargo.toml index 87b1060a8..e0d1bedce 100644 --- a/tools/dylint_lints/Cargo.toml +++ b/tools/dylint_lints/Cargo.toml @@ -63,8 +63,10 @@ serde_json = "1.0" utoipa = "5.2" toolkit-macros = { package = "cf-gears-toolkit-macros", path = "../../libs/toolkit-macros" } toolkit = { package = "cf-gears-toolkit", path = "../../libs/toolkit" } -gts = "0.10.0" -gts-macros = "0.10.0" +toolkit-gts = { package = "cf-gears-toolkit-gts", path = "../../libs/toolkit-gts" } +# GTS library. +gts = "0.11.0" +gts-macros = "0.11.0" schemars = { version = "1.2", features = ["derive"] } # Dev dependencies for lint tests diff --git a/tools/dylint_lints/de01_contract_layer/de0110_no_schema_for_on_gts_structs/src/lib.rs b/tools/dylint_lints/de01_contract_layer/de0110_no_schema_for_on_gts_structs/src/lib.rs index ab708f2c4..5535b3de3 100644 --- a/tools/dylint_lints/de01_contract_layer/de0110_no_schema_for_on_gts_structs/src/lib.rs +++ b/tools/dylint_lints/de01_contract_layer/de0110_no_schema_for_on_gts_structs/src/lib.rs @@ -19,7 +19,7 @@ dylint_linting::declare_late_lint! { /// ### Why is this bad? /// /// GTS-wrapped structs (those using `#[struct_to_gts_schema]`) must use - /// `gts_json_schema_with_refs()` for schema generation because: + /// `gts_schema_with_refs_as_string()` for schema generation because: /// /// 1. **Performance**: It is static (computed at compile time), so it's faster /// 2. **Correct `$id`**: It automatically sets the correct `$id` field @@ -43,21 +43,19 @@ dylint_linting::declare_late_lint! { /// #[struct_to_gts_schema(...)] /// pub struct MyPluginSpec { ... } /// - /// let schema = MyPluginSpec::gts_json_schema_with_refs(); + /// let schema = MyPluginSpec::gts_schema_with_refs_as_string(); /// ``` pub DE0110_NO_SCHEMA_FOR_ON_GTS_STRUCTS, Deny, - "GTS structs must use gts_json_schema_with_refs() instead of schema_for!() (DE0110)" + "GTS structs must use gts_schema_with_refs_as_string() instead of schema_for!() (DE0110)" } -/// Check if a type has the gts_schema_with_refs_as_string method. +/// Check if a type has the `gts_schema_with_refs_as_string` method. /// GTS types generated by `#[struct_to_gts_schema]` have this method. fn is_gts_type<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool { - // Get the ADT (struct/enum) definition if this is one if let ty::Adt(adt_def, _) = ty.kind() { let def_id = adt_def.did(); - // Check if this type has a method named gts_schema_with_refs_as_string let gts_method = Symbol::intern("gts_schema_with_refs_as_string"); for item in cx.tcx.inherent_impls(def_id).iter() { @@ -124,7 +122,7 @@ impl<'tcx> LateLintPass<'tcx> for De0110NoSchemaForOnGtsStructs { ), |diag| { diag.help(format!( - "use `{}::gts_json_schema_with_refs()` instead for proper `$id` and `$ref` handling", + "use `{}::gts_schema_with_refs_as_string()` instead for proper `$id` and `$ref` handling", type_name )); }, diff --git a/tools/dylint_lints/de01_contract_layer/de0110_no_schema_for_on_gts_structs/ui/gts_struct_schema_for.rs b/tools/dylint_lints/de01_contract_layer/de0110_no_schema_for_on_gts_structs/ui/gts_struct_schema_for.rs index 9beaad997..b3abe6c24 100644 --- a/tools/dylint_lints/de01_contract_layer/de0110_no_schema_for_on_gts_structs/ui/gts_struct_schema_for.rs +++ b/tools/dylint_lints/de01_contract_layer/de0110_no_schema_for_on_gts_structs/ui/gts_struct_schema_for.rs @@ -1,13 +1,13 @@ //! Test case: schema_for! on GTS-wrapped struct should trigger DE0110 -use gts_macros::struct_to_gts_schema; +use gts_macros::{gts_id, struct_to_gts_schema}; /// A GTS-wrapped struct (has struct_to_gts_schema attribute) #[derive(Debug, Clone)] #[struct_to_gts_schema( dir_path = "schemas", base = true, - type_id = "gts.cf.core.test.plugin.v1~", + type_id = gts_id!("cf.core.test.plugin.v1~"), description = "Test plugin specification", properties = "id,vendor" )] diff --git a/tools/dylint_lints/de01_contract_layer/de0110_no_schema_for_on_gts_structs/ui/gts_struct_schema_for.stderr b/tools/dylint_lints/de01_contract_layer/de0110_no_schema_for_on_gts_structs/ui/gts_struct_schema_for.stderr index 8d29bdfec..b50cac19f 100644 --- a/tools/dylint_lints/de01_contract_layer/de0110_no_schema_for_on_gts_structs/ui/gts_struct_schema_for.stderr +++ b/tools/dylint_lints/de01_contract_layer/de0110_no_schema_for_on_gts_structs/ui/gts_struct_schema_for.stderr @@ -4,7 +4,7 @@ error: do not use `schema_for!(MyGtsPluginSpecV1)` on GTS-wrapped struct (DE0110 LL | let _schema = schemars::schema_for!(MyGtsPluginSpecV1); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | - = help: use `MyGtsPluginSpecV1::gts_json_schema_with_refs()` instead for proper `$id` and `$ref` handling + = help: use `MyGtsPluginSpecV1::gts_schema_with_refs_as_string()` instead for proper `$id` and `$ref` handling = note: `#[deny(de0110_no_schema_for_on_gts_structs)]` on by default error: aborting due to 1 previous error diff --git a/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/Cargo.toml b/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/Cargo.toml index 13b34b0fc..130b4fb4f 100644 --- a/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/Cargo.toml +++ b/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/Cargo.toml @@ -26,6 +26,7 @@ clippy_utils.workspace = true dylint_linting.workspace = true lint_utils.workspace = true gts.workspace = true +toolkit-gts.workspace = true [dev-dependencies] dylint_testing.workspace = true diff --git a/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/README.md b/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/README.md index 3e8bdfc96..48382f1ad 100644 --- a/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/README.md +++ b/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/README.md @@ -9,14 +9,15 @@ Global Type Schema (GTS) identifier. It ensures that: **type schema** (must end with `~`, no wildcards). 2. Arguments passed to `gts_make_instance_id("...")` are valid **instance segment identifiers** (single segment, no wildcards, no `:` or `~`). -3. Any other string literal that starts with `gts.` or appears inside a - colon-separated permission string contains a valid schema/instance chain. +3. Any other string literal that starts with the configured `GTS_ID_PREFIX` + (default `gts.`) or appears inside a colon-separated permission string + contains a valid schema/instance chain. 4. `const`/`static` items holding GTS wildcard strings (`*`) **must** have names ending with `_WILDCARD` — otherwise the lint reports an error. Wildcards (`*`) are only allowed in contexts where they are used as patterns: permission strings, `resource_pattern(...)`, `with_pattern(...)`, -`resolve_to_uuids(...)`, `GtsWildcard::new(...)`, and `str.starts_with(...)`. +`resolve_to_uuids(...)`, `GtsIdPattern::try_new(...)`, and `str.starts_with(...)`. Everywhere else the lint rejects wildcard tokens. Use `#[allow(de0901_gts_string_pattern)]` to suppress the lint: @@ -45,11 +46,11 @@ protects security-critical permission checks. * `resource_pattern("...")`, `with_pattern("...")`, and `resolve_to_uuids(&["..."])` calls also allow wildcards, since they represent pattern matching or resolution contexts. -* `GtsWildcard::new("...")` — arguments are allowed to contain wildcards, since - `GtsWildcard` is explicitly typed to hold pattern values. +* `GtsIdPattern::try_new("...")` — arguments are allowed to contain wildcards, since + `GtsIdPattern` is explicitly typed to hold pattern values. * `const`/`static` items whose names end with `_WILDCARD` may hold GTS wildcard strings (they are allowed and marked as intentional wildcard constants). -* Strings passed to `str.starts_with("gts.")` are ignored. +* Strings passed to `str.starts_with(GTS_ID_PREFIX)` are ignored. * Inline suppressions are supported through `#[allow(de0901_gts_string_pattern)]` on a binding or expression when a wildcard must be hard-coded outside the recognised helper APIs. @@ -61,11 +62,11 @@ name **must** end with `_WILDCARD`: ```rust // ✅ Allowed — name ends with _WILDCARD -const SRR_WILDCARD: &str = "gts.cf.core.srr.resource.v1~*"; -GtsWildcard::new(SRR_WILDCARD).unwrap(); +const SRR_WILDCARD: &str = gts_id!("cf.core.srr.resource.v1~*"); +GtsIdPattern::try_new(SRR_WILDCARD).unwrap(); // ❌ DE0901: name does not end with _WILDCARD -const SRR_PATTERN: &str = "gts.cf.core.srr.resource.v1~*"; +const SRR_PATTERN: &str = gts_id!("cf.core.srr.resource.v1~*"); // → rename to `SRR_PATTERN_WILDCARD` or use a non-wildcard value ``` @@ -73,35 +74,35 @@ const SRR_PATTERN: &str = "gts.cf.core.srr.resource.v1~*"; ```rust // ❌ Triggers DE0901: wildcard inside a plain schema string -let schema = "gts.acme.core.events.*"; +let schema = gts_id!("acme.core.events.*"); // ❌ Triggers DE0901: schema (with `~`) used in gts_make_instance_id let _id = Product::gts_make_instance_id("vendor.package.sku.some.v1~"); // ❌ Triggers DE0901: const named without _WILDCARD suffix holds a wildcard -const BAD_PATTERN: &str = "gts.cf.core.srr.resource.v1~*"; +const BAD_PATTERN: &str = gts_id!("cf.core.srr.resource.v1~*"); ``` Use instead: ```rust // ✅ Explicit type schema -let schema = "gts.acme.core.events.type.v1~"; +let schema = gts_id!("acme.core.events.type.v1~"); // ✅ Instance id segment let _id = Product::gts_make_instance_id("vendor.package.sku.some.v1"); // ✅ Wildcard allowed inside permission/resource patterns let pattern = Permission::builder() - .resource_pattern("gts.acme.core.events.topic.v1~vendor.*") + .resource_pattern(gts_id!("acme.core.events.topic.v1~vendor.*")) .action("publish") .build() .unwrap(); // ✅ Wildcard constant with _WILDCARD suffix -const ALL_SRR_WILDCARD: &str = "gts.cf.core.srr.resource.v1~*"; -let wc = GtsWildcard::new(ALL_SRR_WILDCARD).unwrap(); +const ALL_SRR_WILDCARD: &str = gts_id!("cf.core.srr.resource.v1~*"); +let wc = GtsIdPattern::try_new(ALL_SRR_WILDCARD).unwrap(); -// ✅ Inline wildcard passed directly to GtsWildcard::new() -let wc = GtsWildcard::new("gts.cf.core.srr.resource.v1~*").unwrap(); +// ✅ Inline wildcard passed directly to GtsIdPattern::try_new() +let wc = GtsIdPattern::try_new(gts_id!("cf.core.srr.resource.v1~*")).unwrap(); ``` diff --git a/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/src/lib.rs b/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/src/lib.rs index d1e4a0e05..0741b1dae 100644 --- a/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/src/lib.rs +++ b/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/src/lib.rs @@ -5,7 +5,7 @@ extern crate rustc_ast; extern crate rustc_span; use clippy_utils::diagnostics::span_lint_and_then; -use gts::{GtsIdSegment, GtsOps}; +use gts::GtsOps; use lint_utils::{filename_str, is_temp_path}; use rustc_ast::token::LitKind; use rustc_ast::{AttrKind, Attribute, Expr, ExprKind, Item, ItemKind}; @@ -13,6 +13,7 @@ use rustc_lint::{EarlyContext, EarlyLintPass, LintContext}; use rustc_span::Span; use std::cell::RefCell; use std::collections::HashSet; +use toolkit_gts::GTS_ID_PREFIX; // Thread-local storage for spans to skip (inside starts_with calls) thread_local! { @@ -59,8 +60,8 @@ impl EarlyLintPass for De0901GtsStringPattern { /// /// | Item name | Value | Result | /// |-------------------|-----------------------------------|---------| - /// | `SRR_WILDCARD` | `"gts.cf.core.srr.resource.v1~*"` | ✅ allowed — name ends with `_WILDCARD` | - /// | `SRR_PATTERN` | `"gts.cf.core.srr.resource.v1~*"` | ❌ flagged — name must end with `_WILDCARD` | + /// | `SRR_WILDCARD` | prefixed GTS wildcard | allowed — name ends with `_WILDCARD` | + /// | `SRR_PATTERN` | prefixed GTS wildcard | flagged — name must end with `_WILDCARD` | /// /// Items with a compliant name are added to the skip set so their value span /// is not re-checked by `check_expr`. @@ -78,11 +79,11 @@ impl EarlyLintPass for De0901GtsStringPattern { }; let Some(init) = init_expr else { return }; - // Only act on GTS wildcard string values (starts with "gts." and contains '*'). + // Only act on GTS wildcard string values. let Some(s) = Self::string_lit_value(init) else { return; }; - if !s.starts_with("gts.") || !s.contains('*') { + if !s.starts_with(GTS_ID_PREFIX) || !s.contains('*') { return; } @@ -96,7 +97,7 @@ impl EarlyLintPass for De0901GtsStringPattern { format!("invalid GTS wildcard pattern in `{item_name}`: '{s}' (DE0901)"), |diag| { diag.note(result.error); - diag.help("Example: gts.cf.core.srr.resource.v1~*"); + diag.help(format!("Example: {GTS_ID_PREFIX}cf.core.srr.resource.v1~*")); }, ); // Still skip-list so check_expr doesn't double-report the literal. @@ -160,7 +161,7 @@ impl EarlyLintPass for De0901GtsStringPattern { || method_name == "resolve_to_uuids" { // Validate nested string literals BEFORE skip-listing so that - // deeply nested GTS strings (e.g. inside &["gts...".to_owned()]) + // deeply nested GTS strings (e.g. inside array literals) // are checked rather than silently escaping validation. for arg in &method_call.args { self.validate_nested_gts_strings(cx, arg, true); @@ -176,8 +177,8 @@ impl EarlyLintPass for De0901GtsStringPattern { } } - // Detect free-function calls: `GtsWildcard::new("...")` or `SomeType::new(...)` where - // the path contains "GtsWildcard". Arguments are allowed to contain wildcards. + // Detect free-function calls: `GtsIdPattern::try_new("...")` or `SomeType::new(...)` where + // the path contains "GtsIdPattern". Arguments are allowed to contain wildcards. if let ExprKind::Call(func, args) = &expr.kind && is_gts_wildcard_new_call(func) { @@ -227,7 +228,7 @@ impl EarlyLintPass for De0901GtsStringPattern { } /// Recursively collect spans from all sub-expressions so that deeply nested -/// string literals (e.g. inside `&["gts...".to_owned()]`) are included in the +/// string literals (e.g. inside nested arrays) are included in the /// skip set. fn collect_nested_spans(expr: &Expr, spans: &mut HashSet) { spans.insert(expr.span); @@ -264,13 +265,13 @@ fn collect_nested_spans(expr: &Expr, spans: &mut HashSet) { } } -/// Returns `true` if `func_expr` is a path call of the form `GtsWildcard::new` -/// (or `gts::GtsWildcard::new`, `::GtsWildcard::new`, etc.). +/// Returns `true` if `func_expr` is a path call of the form `GtsIdPattern::try_new` +/// (or `gts::GtsIdPattern::try_new`, `::GtsIdPattern::try_new`, etc.). /// /// We check that: /// 1. The expression is a `Path` with at least two segments. -/// 2. The last segment is named `new`. -/// 3. At least one other segment is named `GtsWildcard`. +/// 2. The last segment is named `try_new`. +/// 3. At least one other segment is named `GtsIdPattern`. fn is_gts_wildcard_new_call(func_expr: &Expr) -> bool { let ExprKind::Path(_, path) = &func_expr.kind else { return false; @@ -280,12 +281,12 @@ fn is_gts_wildcard_new_call(func_expr: &Expr) -> bool { return false; } let last = segments.last().unwrap(); - if last.ident.name.as_str() != "new" { + if last.ident.name.as_str() != "try_new" { return false; } segments .iter() - .any(|seg| seg.ident.name.as_str() == "GtsWildcard") + .any(|seg| seg.ident.name.as_str() == "GtsIdPattern") } fn is_in_test() -> bool { @@ -428,8 +429,8 @@ impl De0901GtsStringPattern { if let Some(s) = Self::string_lit_value(expr) { let s = s.trim(); - // Option 1: String starts with "gts." - validate directly - if s.starts_with("gts.") { + // Option 1: string starts with the configured GTS prefix - validate directly + if s.starts_with(GTS_ID_PREFIX) { if allow_wildcards { self.validate_any_gts_id_allow_wildcards(cx, expr.span, s); } else { @@ -442,7 +443,7 @@ impl De0901GtsStringPattern { // Permission strings ALWAYS allow wildcards in their GTS parts if s.contains(':') { for part in s.split(':') { - if part.trim().starts_with("gts.") { + if part.trim().starts_with(GTS_ID_PREFIX) { self.validate_any_gts_id_allow_wildcards(cx, expr.span, part.trim()); break; // Only validate the first GTS part found } @@ -533,7 +534,7 @@ impl De0901GtsStringPattern { format!("invalid GTS schema_id: '{}' (DE0901)", s), |diag| { diag.note(result.error); - diag.help("Example: gts.cf.core.events.type.v1~"); + diag.help(format!("Example: {GTS_ID_PREFIX}cf.core.events.type.v1~")); }, ); return; @@ -551,7 +552,7 @@ impl De0901GtsStringPattern { ), |diag| { diag.note("schema_id must end with '~' to indicate it's a type schema"); - diag.help("Example: gts.cf.core.events.type.v1~"); + diag.help(format!("Example: {GTS_ID_PREFIX}cf.core.events.type.v1~")); }, ); } else { @@ -598,34 +599,43 @@ impl De0901GtsStringPattern { return; } - match GtsIdSegment::new(0, 0, s) { - Err(e) => { - span_lint_and_then( - cx, - DE0901_GTS_STRING_PATTERN, - span, - format!("invalid GTS segment: '{}' (DE0901)", s), - |diag| { - diag.note(e.to_string()); - diag.help("Example: vendor.package.sku.abc.v1"); - }, - ); - } - Ok(seg) if !allowed_vendors(cx, span).contains(&seg.vendor.as_str()) => { - span_lint_and_then( - cx, - DE0901_GTS_STRING_PATTERN, - span, - format!("invalid GTS vendor in segment: '{}' (DE0901)", s), - |diag| { - diag.note(format!( - "found vendor '{}', allowed vendors are 'cf' and 'example'", - seg.vendor - )); - }, - ); - } - Ok(_) => {} + // `gts_make_instance_id` takes a single *segment* (no GTS prefix), + // but the parser only exposes full-id validation. Pin a dummy type + // segment in front, parse the whole id, then validate the trailing + // segment (the user-provided one). + let result = GtsOps::parse_id(&format!("{GTS_ID_PREFIX}cf.core.dummy.t.v1~{s}")); + if !result.ok { + span_lint_and_then( + cx, + DE0901_GTS_STRING_PATTERN, + span, + format!("invalid GTS segment: '{}' (DE0901)", s), + |diag| { + diag.note(result.error); + diag.help("Example: vendor.package.sku.abc.v1"); + }, + ); + return; + } + let Some(seg) = result.segments.last() else { + return; + }; + if !seg.vendor.is_empty() + && seg.vendor != "*" + && !allowed_vendors(cx, span).contains(&seg.vendor.as_str()) + { + span_lint_and_then( + cx, + DE0901_GTS_STRING_PATTERN, + span, + format!("invalid GTS vendor in segment: '{}' (DE0901)", s), + |diag| { + diag.note(format!( + "found vendor '{}', allowed vendors are 'cf' and 'example'", + seg.vendor + )); + }, + ); } } diff --git a/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/ui/invalid_cases.rs b/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/ui/invalid_cases.rs index 0e823f8b4..e875185d7 100644 --- a/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/ui/invalid_cases.rs +++ b/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/ui/invalid_cases.rs @@ -8,7 +8,7 @@ use gts_macros::struct_to_gts_schema; dir_path = "schemas", base = true, // Should NOT trigger - valid GTS schema_id string - type_id = "gts.vendor.test.entities.product.v1~", + type_id = gts_id!("vendor.test.entities.product.v1~"), description = "Product entity", properties = "id" )] diff --git a/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/ui/invalid_cases.stderr b/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/ui/invalid_cases.stderr index d34f12e47..15ec7fa33 100644 --- a/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/ui/invalid_cases.stderr +++ b/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/ui/invalid_cases.stderr @@ -13,7 +13,7 @@ error: invalid GTS segment: 'vendor.package.sku.v1' (DE0901) LL | let _id2 = ProductV1::<()>::gts_make_instance_id("vendor.package.sku.v1"); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | - = note: Invalid GTS segment #0 @ offset 0: 'vendor.package.sku.v1': Too few tokens (got 4, min 5). Expected format: vendor.package.namespace.type.vMAJOR[.MINOR] + = note: Invalid GTS segment #2 @ offset 23: 'vendor.package.sku.v1': Too few tokens (got 4, min 5). Expected format: vendor.package.namespace.type.vMAJOR[.MINOR] = help: Example: vendor.package.sku.abc.v1 error: gts_make_instance_id expects a single GTS segment, got: 'vendor.package.sku.abc.v1~' (DE0901) diff --git a/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/ui/permission_strings.rs b/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/ui/permission_strings.rs index b679b3a13..796be2e9a 100644 --- a/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/ui/permission_strings.rs +++ b/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/ui/permission_strings.rs @@ -32,7 +32,7 @@ fn main() { let _perm6 = "uuid:gts.badvendor.pkg.ns.type.v1~cf.pkg.ns.derived.v1~:action"; let _perm7 = MockPermissionBuilder::default() - // Should trigger DE0901 - invalid GTS + // Should NOT trigger DE0901 - wildcards are allowed in resource_pattern() calls .resource_pattern("gts.cf.core.events.type.v*") .build(); diff --git a/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/ui/permission_strings.stderr b/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/ui/permission_strings.stderr index 60f9de049..bc79d2db5 100644 --- a/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/ui/permission_strings.stderr +++ b/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/ui/permission_strings.stderr @@ -4,7 +4,7 @@ error: invalid GTS string: 'gts.cf.*.events.*.v1~' (DE0901) LL | let _perm3 = "resource-id:gts.cf.*.events.*.v1~:action:scope"; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | - = note: Invalid GTS wildcard pattern: gts.cf.*.events.*.v1~: The wildcard '*' token is allowed only once + = note: Invalid GTS identifier: gts.cf.*.events.*.v1~: The wildcard '*' token is allowed only once = note: `#[deny(de0901_gts_string_pattern)]` on by default error: invalid GTS string: 'gts.cf.core.events.event.v1~a.b.c~' (DE0901) @@ -39,13 +39,5 @@ LL | let _perm6 = "uuid:gts.badvendor.pkg.ns.type.v1~cf.pkg.ns.derived.v1~:a | = note: found vendor 'badvendor', allowed vendors are 'cf' and 'example' -error: invalid GTS string: 'gts.cf.core.events.type.v*' (DE0901) - --> $DIR/permission_strings.rs:36:27 - | -LL | .resource_pattern("gts.cf.core.events.type.v*") - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: Invalid GTS wildcard pattern: gts.cf.core.events.type.v*: The wildcard '*' token is allowed only at the end of the pattern - -error: aborting due to 6 previous errors +error: aborting due to 5 previous errors diff --git a/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/ui/valid_use_cases.rs b/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/ui/valid_use_cases.rs index 299bcb0ae..0daa438c4 100644 --- a/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/ui/valid_use_cases.rs +++ b/tools/dylint_lints/de09_gts_layer/de0901_gts_string_pattern/ui/valid_use_cases.rs @@ -1,14 +1,15 @@ // Test file for valid GTS strings and gts-macros annotations - should not trigger DE0901 -use gts::{GtsInstanceId, GtsWildcard}; +use gts::{GtsIdPattern, GtsInstanceId}; use gts_macros::struct_to_gts_schema; +use toolkit_gts::{GTS_ID_PREFIX, gts_id}; #[derive(Debug)] #[struct_to_gts_schema( dir_path = "schemas", base = true, // Should NOT trigger DE0901 - valid GTS schema_id string - type_id = "gts.example.core.events.topic.v1~", + type_id = gts_id!("example.core.events.topic.v1~"), description = "Event Topic definition", properties = "id,name" )] @@ -23,7 +24,7 @@ pub struct EventTopicV1 { dir_path = "schemas", base = true, // Should NOT trigger DE0901- valid GTS schema_id string - type_id = "gts.example.core.events.type.v1~", + type_id = gts_id!("example.core.events.type.v1~"), description = "Base event type definition", properties = "id" )] @@ -37,7 +38,7 @@ pub struct BaseEventTypeV1 { dir_path = "schemas", base = BaseEventTypeV1, // Should NOT trigger DE0901 - valid GTS schema_id string with inheritance - type_id = "gts.example.core.events.type.v1~cf.core.audit.event.v1~", + type_id = gts_id!("example.core.events.type.v1~cf.core.audit.event.v1~"), description = "Audit event", properties = "user_id" )] @@ -46,41 +47,48 @@ pub struct AuditEventV1 { } // Should NOT trigger DE0901 - wildcard const has _WILDCARD suffix -const SRR_WILDCARD: &str = "gts.example.core.srr.resource.v1~*"; +fn srr_wildcard() -> String { + format!("{GTS_ID_PREFIX}example.core.srr.resource.v1~*") +} fn main() { // Should NOT trigger DE0901 - valid GTS instance segment let _id = EventTopicV1::<()>::gts_make_instance_id("example.commerce.orders.orders.v1.0"); // Should NOT trigger DE0901 - valid GTS type schema string - let _s1 = "gts.example.core.events.type.v1~"; + let _s1 = gts_id!("example.core.events.type.v1~"); // Should NOT trigger DE0901 - valid GTS type schema string with inheritance - let _s2 = "gts.example.core.events.type.v1~cf.core.audit.event.v1~"; + let _s2 = gts_id!("example.core.events.type.v1~cf.core.audit.event.v1~"); // Should NOT trigger DE0901 - strings inside starts_with() should be ignored - let _check = "some.invalid.gts.string".starts_with("gts."); + let _check = "some.invalid.gts.string".starts_with(GTS_ID_PREFIX); // Should NOT trigger DE0901 - strings inside starts_with() should be ignored - let _check2 = "another.invalid.gts.string".starts_with("gts.example.core."); + let _check2 = + "another.invalid.gts.string".starts_with(&format!("{GTS_ID_PREFIX}example.core.")); - // Should NOT trigger DE0901 - GtsWildcard::new() accepts wildcard patterns - let _wc1 = GtsWildcard::new("gts.example.core.srr.resource.v1~*"); + // Should NOT trigger DE0901 - GtsIdPattern::try_new() accepts wildcard patterns + let _wc1 = GtsIdPattern::try_new(&format!("{GTS_ID_PREFIX}example.core.srr.resource.v1~*")); - // Should NOT trigger DE0901 - GtsWildcard::new() accepts wildcard with sub-prefix - let _wc2 = GtsWildcard::new("gts.example.core.srr.resource.v1~example.*"); + // Should NOT trigger DE0901 - GtsIdPattern::try_new() accepts wildcard with sub-prefix + let _wc2 = GtsIdPattern::try_new(&format!( + "{GTS_ID_PREFIX}example.core.srr.resource.v1~example.*" + )); - // Should NOT trigger DE0901 - gts::GtsWildcard::new() qualified path form - let _wc3 = gts::GtsWildcard::new("gts.example.core.events.type.v1~*"); + // Should NOT trigger DE0901 - gts::GtsIdPattern::try_new() qualified path form + let _wc3 = gts::GtsIdPattern::try_new(&format!("{GTS_ID_PREFIX}example.core.events.type.v1~*")); - // Should NOT trigger DE0901 - const holding wildcard used with GtsWildcard::new() - let _wc4 = GtsWildcard::new(SRR_WILDCARD); + // Should NOT trigger DE0901 - const holding wildcard used with GtsIdPattern::try_new() + let _wc4 = GtsIdPattern::try_new(&srr_wildcard()); } // Vendor checks are skipped in #[cfg(test)] modules - any vendor is allowed #[cfg(test)] mod test_vendors_allowed { + use toolkit_gts::gts_id; + // Should NOT trigger DE0901 - vendor checks are skipped in test code fn _test_acme_vendor() { - let _s = "gts.acme.core.events.type.v1~"; + let _s = gts_id!("acme.core.events.type.v1~"); } } diff --git a/tools/dylint_lints/de09_gts_layer/de0902_no_schema_for_on_gts_structs/ui/gts_struct_schema_for.rs b/tools/dylint_lints/de09_gts_layer/de0902_no_schema_for_on_gts_structs/ui/gts_struct_schema_for.rs index 8106f06b9..3d6478864 100644 --- a/tools/dylint_lints/de09_gts_layer/de0902_no_schema_for_on_gts_structs/ui/gts_struct_schema_for.rs +++ b/tools/dylint_lints/de09_gts_layer/de0902_no_schema_for_on_gts_structs/ui/gts_struct_schema_for.rs @@ -1,13 +1,13 @@ //! Test case: schema_for! on GTS-wrapped struct should trigger DE0902 -use gts_macros::struct_to_gts_schema; +use gts_macros::{gts_id, struct_to_gts_schema}; /// A GTS-wrapped struct (has struct_to_gts_schema attribute) #[derive(Debug, Clone)] #[struct_to_gts_schema( dir_path = "schemas", base = true, - type_id = "gts.cf.core.test.plugin.v1~", + type_id = gts_id!("cf.core.test.plugin.v1~"), description = "Test plugin specification", properties = "id,vendor" )] diff --git a/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/all_features_true/Cargo.lock b/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/all_features_true/Cargo.lock deleted file mode 100644 index 7d1e23c15..000000000 --- a/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/all_features_true/Cargo.lock +++ /dev/null @@ -1,7 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "de1201_all_features_true" -version = "0.1.0" diff --git a/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/excluded_crate/Cargo.lock b/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/excluded_crate/Cargo.lock deleted file mode 100644 index 2c0f2a82b..000000000 --- a/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/excluded_crate/Cargo.lock +++ /dev/null @@ -1,7 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "de1201_excluded_crate" -version = "0.1.0" diff --git a/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/missing_docs_rs/Cargo.lock b/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/missing_docs_rs/Cargo.lock deleted file mode 100644 index 3daa52a41..000000000 --- a/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/missing_docs_rs/Cargo.lock +++ /dev/null @@ -1,7 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "de1201_missing_docs_rs" -version = "0.1.0" diff --git a/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/publish_false/Cargo.lock b/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/publish_false/Cargo.lock deleted file mode 100644 index b0286bc7b..000000000 --- a/tools/dylint_lints/de12_documentation/de1201_docs_rs_all_features/tests/fixtures/publish_false/Cargo.lock +++ /dev/null @@ -1,7 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "de1201_publish_false" -version = "0.1.0" diff --git a/tools/fuzz/Cargo.lock b/tools/fuzz/Cargo.lock index 87002e06f..8b4dd9d02 100644 --- a/tools/fuzz/Cargo.lock +++ b/tools/fuzz/Cargo.lock @@ -2,15 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - [[package]] name = "android_system_properties" version = "0.1.5" @@ -51,27 +42,12 @@ dependencies = [ "num-traits", ] -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - [[package]] name = "bumpalo" version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" -[[package]] -name = "bytes" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" - [[package]] name = "cc" version = "1.2.53" @@ -85,40 +61,46 @@ dependencies = [ ] [[package]] -name = "cf-gears-toolkit-errors" -version = "0.5.1" +name = "cf-gears-fuzz" +version = "0.0.0" dependencies = [ - "http", + "cf-gears-toolkit-odata", + "libfuzzer-sys", +] + +[[package]] +name = "cf-gears-toolkit-canonical-errors" +version = "0.7.4" +dependencies = [ + "cf-gears-toolkit-canonical-errors-macro", "serde", + "serde_json", + "thiserror", ] [[package]] -name = "cf-gears-toolkit-errors-macro" -version = "0.5.1" +name = "cf-gears-toolkit-canonical-errors-macro" +version = "0.6.1" dependencies = [ + "proc-macro-crate", "proc-macro2", "quote", - "serde", - "serde_json", "syn", ] [[package]] name = "cf-gears-toolkit-odata" -version = "0.5.1" +version = "0.8.1" dependencies = [ "base64", "bigdecimal", - "cf-gears-toolkit-errors", - "cf-gears-toolkit-errors-macro", + "cf-gears-toolkit-canonical-errors", "chrono", - "hex", - "http", - "odata-params", + "chrono-tz", + "peg", "serde", "serde_json", - "sha2", - "thiserror 2.0.18", + "thiserror", "uuid", ] @@ -135,33 +117,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ "iana-time-zone", - "js-sys", "num-traits", "serde", - "wasm-bindgen", "windows-link", ] [[package]] name = "chrono-tz" -version = "0.9.0" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb" +checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3" dependencies = [ "chrono", - "chrono-tz-build", - "phf", -] - -[[package]] -name = "chrono-tz-build" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1" -dependencies = [ - "parse-zoneinfo", "phf", - "phf_codegen", ] [[package]] @@ -171,33 +139,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crypto-common" -version = "0.1.7" +name = "equivalent" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "find-msvc-tools" @@ -205,16 +150,6 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - [[package]] name = "getrandom" version = "0.3.4" @@ -228,28 +163,10 @@ dependencies = [ ] [[package]] -name = "hex" -version = "0.4.3" +name = "hashbrown" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "http" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "cf-gears-fuzz" -version = "0.0.0" -dependencies = [ - "cf-gears-toolkit-odata", - "libfuzzer-sys", -] +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "iana-time-zone" @@ -275,6 +192,16 @@ dependencies = [ "cc", ] +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "itoa" version = "1.0.17" @@ -363,35 +290,12 @@ dependencies = [ "autocfg", ] -[[package]] -name = "odata-params" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d32ae3978a6fc4114aec5b7907038efa81452336845a98a1e2ec957c8f32ade6" -dependencies = [ - "bigdecimal", - "chrono", - "chrono-tz", - "peg", - "thiserror 1.0.69", - "uuid", -] - [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" -[[package]] -name = "parse-zoneinfo" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" -dependencies = [ - "regex", -] - [[package]] name = "peg" version = "0.8.5" @@ -421,40 +325,29 @@ checksum = "132dca9b868d927b35b5dd728167b2dee150eb1ad686008fc71ccb298b776fca" [[package]] name = "phf" -version = "0.11.3" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" dependencies = [ "phf_shared", ] [[package]] -name = "phf_codegen" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" -dependencies = [ - "phf_generator", - "phf_shared", -] - -[[package]] -name = "phf_generator" -version = "0.11.3" +name = "phf_shared" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981" dependencies = [ - "phf_shared", - "rand", + "siphasher", ] [[package]] -name = "phf_shared" -version = "0.11.3" +name = "proc-macro-crate" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "siphasher", + "toml_edit", ] [[package]] @@ -481,50 +374,6 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" - -[[package]] -name = "regex" -version = "1.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" - [[package]] name = "rustversion" version = "1.0.22" @@ -574,17 +423,6 @@ dependencies = [ "zmij", ] -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "shlex" version = "1.3.0" @@ -608,29 +446,20 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.18", + "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.69" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -638,21 +467,34 @@ dependencies = [ ] [[package]] -name = "thiserror-impl" -version = "2.0.18" +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ - "proc-macro2", - "quote", - "syn", + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", ] [[package]] -name = "typenum" -version = "1.19.0" +name = "toml_parser" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow", +] [[package]] name = "unicode-ident" @@ -662,21 +504,15 @@ checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "uuid" -version = "1.19.0" +version = "1.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" dependencies = [ "js-sys", "serde_core", "wasm-bindgen", ] -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - [[package]] name = "wasip2" version = "1.0.2+wasi-0.2.9" @@ -790,6 +626,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.51.0" diff --git a/tools/gts-analyze/Cargo.toml b/tools/gts-analyze/Cargo.toml index f82245f92..ddd36f28f 100644 --- a/tools/gts-analyze/Cargo.toml +++ b/tools/gts-analyze/Cargo.toml @@ -22,3 +22,8 @@ clap = { workspace = true } walkdir = { workspace = true } regex = { workspace = true } anyhow = { workspace = true } +# For `GtsId` parsing and the upstream maximum GTS-id length constant. +gts-id = { workspace = true } +# For the compile-time configured GTS ID prefixes re-exported through the +# toolkit GTS facade. +toolkit-gts = { workspace = true } diff --git a/tools/gts-analyze/src/scan/json.rs b/tools/gts-analyze/src/scan/json.rs index b859f6545..695776a4a 100644 --- a/tools/gts-analyze/src/scan/json.rs +++ b/tools/gts-analyze/src/scan/json.rs @@ -2,10 +2,11 @@ //! GTS-id string-literal references from the JSON body. use std::path::Path; +use toolkit_gts::GTS_ID_URI_PREFIX; use crate::classify::classify_location; use crate::model::{Reference, TypeDef}; -use crate::scan::{gts_in_string_re, line_at, looks_like_gts_id, shorten_line}; +use crate::scan::{gts_in_string_re, line_at, shorten_line}; /// Scan a JSON file. Pushes a TypeDef when the document is a GTS schema (`$id` matches), /// and a Reference for every other GTS string literal it contains. @@ -34,16 +35,14 @@ fn scan_schema(rel: &str, location: &str, text: &str) -> Option { let obj = data.as_object()?; let sid = obj.get("$id")?.as_str()?; let sid = sid - .strip_prefix("gts://") + .strip_prefix(GTS_ID_URI_PREFIX) .unwrap_or(sid) .split('?') .next()? .split('#') .next()?; - if !sid.starts_with("gts.") { - return None; - } - if !looks_like_gts_id(sid) { + let parsed = gts_id::GtsId::try_new(sid).ok()?; + if !parsed.is_type() { return None; } let description = obj diff --git a/tools/gts-analyze/src/scan/mod.rs b/tools/gts-analyze/src/scan/mod.rs index 529444072..7083cd1cf 100644 --- a/tools/gts-analyze/src/scan/mod.rs +++ b/tools/gts-analyze/src/scan/mod.rs @@ -4,6 +4,7 @@ pub mod rust; use regex::Regex; use std::sync::OnceLock; +use toolkit_gts::GTS_ID_PREFIX; /// One GTS-segment grammar (matches the spec subset used elsewhere in the project). /// Form: `....v[.]`. @@ -25,7 +26,8 @@ pub fn gts_in_string_re() -> &'static Regex { static R: OnceLock = OnceLock::new(); R.get_or_init(|| { let body = id_with_wildcard(); - Regex::new(&format!(r#""(gts\.{body})""#)).expect("static regex") + let prefix = regex::escape(GTS_ID_PREFIX); + Regex::new(&format!(r#""({prefix}{body})""#)).expect("static regex") }) } @@ -38,14 +40,21 @@ pub fn gts_bare_re() -> &'static Regex { static R: OnceLock = OnceLock::new(); R.get_or_init(|| { let body = id_with_wildcard(); - Regex::new(&format!(r"(?:^|[^a-z0-9_])(gts\.{body})")).expect("static regex") + let prefix = regex::escape(GTS_ID_PREFIX); + Regex::new(&format!(r"(?:^|[^a-z0-9_])({prefix}{body})")).expect("static regex") }) } /// True if a string passes the loose GTS-id heuristic used by the Rust scanner — /// permits chained ids and `~`-suffixes, not a strict spec validator. pub fn looks_like_gts_id(s: &str) -> bool { - if !s.starts_with("gts.") || s.len() > 512 { + if s.len() > gts_id::GTS_ID_MAX_LENGTH { + return false; + } + // Use the compile-time configured prefix instead of a + // hard-coded literal so the heuristic tracks whatever prefix the GTS-id + // crate was built with. + if !s.starts_with(GTS_ID_PREFIX) { return false; } s.contains(".v")