Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gts-spec
Submodule .gts-spec updated 53 files
+15 −0 NOTICE
+281 −41 README.md
+6 −0 examples/events/README.md
+24 −0 ...s/events/instances/gts.x.core.events.type_combined_id.v1~x.commerce.orders.order_placed.v1.0~.examples.json
+16 −7 examples/events/schemas/gts.x.core.events.type.v1~.schema.json
+4 −0 examples/events/schemas/gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.0~.schema.json
+4 −0 examples/events/schemas/gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.1~.schema.json
+4 −3 examples/events/schemas/gts.x.core.events.type.v1~x.core.idp.contact_created.v1~.schema.json
+88 −0 examples/events/schemas/gts.x.core.events.type_combined.v1~.schema.json
+33 −0 examples/events/schemas/gts.x.core.events.type_combined.v1~x.commerce.orders.order_placed.v1.0~.schema.json
+16 −6 examples/modules/instances/gts.x.core.modules.module.v1~x.webstore._.catalog.v1.json
+7 −5 examples/modules/instances/gts.x.core.modules.module.v1~x.webstore._.chat.v1.json
+7 −4 examples/modules/schemas/gts.x.core.modules.capability.v1~.schema.json
+78 −0 examples/typespec/vms/README.md
+6 −0 examples/typespec/vms/instances/states/gts.x.infra.compute.vm_state.v1~x.infra._.migrating.v1.json
+6 −0 examples/typespec/vms/instances/states/gts.x.infra.compute.vm_state.v1~x.infra._.paused.v1.json
+6 −0 examples/typespec/vms/instances/states/gts.x.infra.compute.vm_state.v1~x.infra._.rebooting.v1.json
+6 −0 examples/typespec/vms/instances/states/gts.x.infra.compute.vm_state.v1~x.infra._.running.v1.json
+6 −0 examples/typespec/vms/instances/states/gts.x.infra.compute.vm_state.v1~x.infra._.starting.v1.json
+6 −0 examples/typespec/vms/instances/states/gts.x.infra.compute.vm_state.v1~x.infra._.stopped.v1.json
+6 −0 examples/typespec/vms/instances/states/gts.x.infra.compute.vm_state.v1~x.infra._.stopping.v1.json
+6 −0 examples/typespec/vms/instances/states/gts.x.infra.compute.vm_state.v1~x.infra._.suspended.v1.json
+6 −0 examples/typespec/vms/instances/states/gts.x.infra.compute.vm_state.v1~x.infra._.suspending.v1.json
+28 −0 examples/typespec/vms/instances/vms/gts.x.infra.compute.vm.v1~nutanix.ahv._.vm.v1~db-server-01.json
+24 −0 examples/typespec/vms/instances/vms/gts.x.infra.compute.vm.v1~vmware.esxi._.vm.v1~web-server-01.json
+26 −0 examples/typespec/vms/instances/vms/gts.x.infra.compute.vm.v1~vz.vz._.vm.v1~app-server-01.json
+169 −0 examples/typespec/vms/schemas/common.tsp
+72 −0 examples/typespec/vms/schemas/gts.x.infra.compute.vm.v1~.schema.json
+83 −0 examples/typespec/vms/schemas/gts.x.infra.compute.vm.v1~.tsp
+76 −0 examples/typespec/vms/schemas/gts.x.infra.compute.vm.v1~nutanix.ahv._.vm.v1~.schema.json
+47 −0 examples/typespec/vms/schemas/gts.x.infra.compute.vm.v1~nutanix.ahv._.vm.v1~.tsp
+73 −0 examples/typespec/vms/schemas/gts.x.infra.compute.vm.v1~vmware.esxi._.vm.v1~.schema.json
+47 −0 examples/typespec/vms/schemas/gts.x.infra.compute.vm.v1~vmware.esxi._.vm.v1~.tsp
+72 −0 examples/typespec/vms/schemas/gts.x.infra.compute.vm.v1~vz.vz._.vm.v1~.schema.json
+47 −0 examples/typespec/vms/schemas/gts.x.infra.compute.vm.v1~vz.vz._.vm.v1~.tsp
+30 −0 examples/typespec/vms/schemas/states/gts.x.infra.compute.vm_state.v1~.schema.json
+36 −0 examples/typespec/vms/schemas/states/gts.x.infra.compute.vm_state.v1~.tsp
+138 −0 examples/yaml/ui/README.md
+114 −0 examples/yaml/ui/instances/gts.x.ui.core.item.v1~x.ui.components.grid.v1~users_list.yaml
+27 −0 examples/yaml/ui/instances/gts.x.ui.core.item.v1~x.ui.components.menu_item.v1~main_dashboard.yaml
+33 −0 examples/yaml/ui/instances/gts.x.ui.core.item.v1~x.ui.components.menu_item.v1~user_settings.yaml
+86 −0 examples/yaml/ui/schemas/gts.x.ui.core.item.v1~.schema.yaml
+185 −0 examples/yaml/ui/schemas/gts.x.ui.core.item.v1~x.ui.components.grid.v1~.schema.yaml
+106 −0 examples/yaml/ui/schemas/gts.x.ui.core.item.v1~x.ui.components.menu_item.v1~.schema.yaml
+114 −0 tests/openapi.json
+3,574 −0 tests/test_op12_schema_vs_schema_validation.py
+2,147 −0 tests/test_op13_schema_traits_validation.py
+10 −0 tests/test_op1_id_validation.py
+44 −0 tests/test_op2_id_extraction.py
+28 −0 tests/test_op2_schema_id_priority.py
+36 −0 tests/test_op3_id_parsing.py
+24 −0 tests/test_op5_id_uuid.py
+637 −0 tests/test_refimpl_x_gts_ref.py
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ Featureset:
- [x] **OP#9 - Version Casting**: Transform instances between compatible MINOR versions
- [x] **OP#10 - Query Execution**: Filter identifier collections using the GTS query language
- [x] **OP#11 - Attribute Access**: Retrieve property values and metadata using the attribute selector (`@`)
- [ ] **OP#12 - Schema Validation**: Validate schema against its precedent schema
- [x] **OP#12 - Schema Validation**: Validate schema against its precedent schema
- [x] **OP#13 - Schema Traits Validation**: Validate `x-gts-traits-schema` / `x-gts-traits` across the inheritance chain

TODO - need a file with Go code snippets for all Ops above

Expand Down Expand Up @@ -222,6 +223,13 @@ gts -path ./examples query -expr "gts.vendor.pkg.*" -limit 10
# OP#10 - Get attribute value
gts -path ./examples attr -path gts.vendor.pkg.ns.type.v1.0@name

# OP#12 - Validate a derived schema against its chain
gts -path ./examples validate-schema -id gts.vendor.pkg.ns.base.v1~derived.v1~

# OP#13 - Validate any entity (schema or instance)
gts -path ./examples validate-entity -id gts.vendor.pkg.ns.base.v1~derived.v1~
gts -path ./examples validate-entity -id gts.vendor.pkg.ns.type.v1.0

# List all entities
gts -path ./examples list -limit 100

Expand Down
4 changes: 4 additions & 0 deletions cmd/gts/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ The commands are:
match-id-pattern match a GTS ID against a pattern
uuid generate UUID from a GTS ID
validate validate an instance against its schema
validate-schema validate a derived schema against its chain
validate-entity validate any entity (schema or instance) including traits
relationships resolve relationships for an entity
compatibility check compatibility between two schemas
cast cast an instance to a target schema
Expand Down Expand Up @@ -83,6 +85,8 @@ var commands = []*Command{
cmdMatchIDPattern,
cmdUUID,
cmdValidate,
cmdValidateSchema,
cmdValidateEntity,
cmdRelationships,
cmdCompatibility,
cmdCast,
Expand Down
43 changes: 43 additions & 0 deletions cmd/gts/validate_entity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
Copyright © 2025 Global Type System
Released under Apache License 2.0
*/

package main

var cmdValidateEntity = &Command{
UsageLine: "validate-entity -id <gts-id>",
Short: "validate any entity (schema or instance) including traits",
Long: `
ValidateEntity validates any GTS entity by its ID.

For schemas: runs OP#12 chain validation and OP#13 traits validation.
For instances: validates the instance against its schema.

The -id flag specifies the GTS entity ID to validate.
Requires -path to be set to load entities.

Example:

gts -path ./examples validate-entity -id gts.vendor.pkg.ns.base.v1~derived.v1~
gts -path ./examples validate-entity -id gts.vendor.pkg.ns.type.v1.0
`,
}

var validateEntityID string

func init() {
cmdValidateEntity.Run = runValidateEntity
cmdValidateEntity.Flag.StringVar(&validateEntityID, "id", "", "GTS entity ID to validate")
}

func runValidateEntity(cmd *Command, args []string) {
if validateEntityID == "" {
cmd.Usage()
return
}

store := newStore()
result := store.ValidateEntity(validateEntityID)
writeJSON(result)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
40 changes: 40 additions & 0 deletions cmd/gts/validate_schema.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
Copyright © 2025 Global Type System
Released under Apache License 2.0
*/

package main

var cmdValidateSchema = &Command{
UsageLine: "validate-schema -id <gts-id>",
Short: "validate a derived schema against its chain",
Long: `
ValidateSchema checks that a derived schema only tightens, never loosens,
the constraints defined by its base schema(s).

The -id flag specifies the chained GTS schema ID to validate.
Requires -path to be set to load entities.

Example:

gts -path ./examples validate-schema -id gts.vendor.pkg.ns.base.v1~derived.v1~
`,
}

var validateSchemaID string

func init() {
cmdValidateSchema.Run = runValidateSchema
cmdValidateSchema.Flag.StringVar(&validateSchemaID, "id", "", "chained GTS schema ID to validate")
}

func runValidateSchema(cmd *Command, args []string) {
if validateSchemaID == "" {
cmd.Usage()
return
}

store := newStore()
result := store.ValidateSchemaChain(validateSchemaID)
writeJSON(result)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
54 changes: 45 additions & 9 deletions gts/gts.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ type GtsIDSegment struct {
VerMinor *int
IsType bool
IsWildcard bool
IsUUID bool
}

// GtsID represents a validated GTS identifier
Expand All @@ -95,11 +96,6 @@ func NewGtsID(id string) (*GtsID, error) {
return nil, &InvalidGtsIDError{GtsID: id, Cause: "Must be lower case"}
}

// Validate no hyphens
if strings.Contains(raw, "-") {
return nil, &InvalidGtsIDError{GtsID: id, Cause: "Must not contain '-'"}
}

// Validate prefix
if !strings.HasPrefix(raw, GtsPrefix) {
return nil, &InvalidGtsIDError{GtsID: id, Cause: fmt.Sprintf("Does not start with '%s'", GtsPrefix)}
Expand All @@ -110,21 +106,48 @@ func NewGtsID(id string) (*GtsID, error) {
return nil, &InvalidGtsIDError{GtsID: id, Cause: "Too long"}
}

// Split by ~ to get segments, preserving empties to detect trailing ~
remainder := raw[len(GtsPrefix):]
parts := splitPreservingTilde(remainder)

// Detect combined anonymous instance: last part is a UUID (no trailing ~)
hasUUIDTail := len(parts) >= 2 && isUUIDSegment(parts[len(parts)-1])

// Validate no hyphens in non-UUID portions (reuse already-split parts)
gtsPartsToCheck := parts
if hasUUIDTail {
gtsPartsToCheck = parts[:len(parts)-1]
}
for _, p := range gtsPartsToCheck {
if strings.Contains(p, "-") {
return nil, &InvalidGtsIDError{GtsID: id, Cause: "Must not contain '-'"}
}
}

gtsID := &GtsID{
ID: raw,
Segments: make([]*GtsIDSegment, 0),
}

// Split by ~ to get segments, preserving empties to detect trailing ~
remainder := raw[len(GtsPrefix):]
parts := splitPreservingTilde(remainder)

offset := len(GtsPrefix)
for i, part := range parts {
if part == "" {
return nil, &InvalidGtsIDError{GtsID: id, Cause: fmt.Sprintf("GTS segment #%d @ offset %d is empty", i+1, offset)}
}

// Last part is a UUID tail — store as a special segment
if hasUUIDTail && i == len(parts)-1 {
gtsID.Segments = append(gtsID.Segments, &GtsIDSegment{
Num: i + 1,
Offset: offset,
Segment: part,
IsType: false,
IsUUID: true,
})
offset += len(part)
continue
}

segment, err := parseSegment(i+1, offset, part)
if err != nil {
return nil, err
Expand Down Expand Up @@ -391,3 +414,16 @@ func parseSegment(num, offset int, segment string) (*GtsIDSegment, error) {

return seg, nil
}

// isUUIDSegment returns true if s is a valid RFC 4122 UUID (lowercase hex with hyphens)
func isUUIDSegment(s string) bool {
u, err := uuid.Parse(s)
if err != nil {
return false
}
if u.Variant() != uuid.RFC4122 {
return false
}
// uuid.Parse accepts URN, braced, and uppercase forms; enforce canonical lowercase hyphenated form
return s == strings.ToLower(u.String())
}
53 changes: 53 additions & 0 deletions gts/gts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,3 +251,56 @@ func TestGtsID_TildeNotAtEnd(t *testing.T) {
t.Errorf("Expected error for tilde not at end: %q", invalidID)
}
}

// TestGtsID_CombinedAnonymousInstance tests combined anonymous instance IDs (UUID tail)
func TestGtsID_CombinedAnonymousInstance(t *testing.T) {
validIDs := []string{
"gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.0~7a1d2f34-5678-49ab-9012-abcdef123456",
"gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.0~7a1d2f34-5678-49ab-9012-000000000000",
}

for _, id := range validIDs {
t.Run(id, func(t *testing.T) {
gtsID, err := NewGtsID(id)
if err != nil {
t.Fatalf("Expected valid combined anonymous instance ID %q, got error: %v", id, err)
}
lastSeg := gtsID.Segments[len(gtsID.Segments)-1]
if !lastSeg.IsUUID {
t.Errorf("Expected last segment IsUUID=true for %q", id)
}
if lastSeg.IsType {
t.Errorf("Expected last segment IsType=false for %q", id)
}
if gtsID.IsType() {
t.Errorf("Expected IsType()=false for combined anonymous instance %q", id)
}
})
}
}

// TestGtsID_CombinedAnonymousInstance_Invalid tests that invalid UUID tails are rejected
func TestGtsID_CombinedAnonymousInstance_Invalid(t *testing.T) {
invalidIDs := []struct {
id string
desc string
}{
{
"gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.0~not-a-uuid",
"non-UUID tail with hyphens",
},
{
"gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.0~7A1D2F34-5678-49AB-9012-ABCDEF123456",
"uppercase UUID tail",
},
}

for _, tt := range invalidIDs {
t.Run(tt.desc, func(t *testing.T) {
_, err := NewGtsID(tt.id)
if err == nil {
t.Errorf("Expected error for %s: %q", tt.desc, tt.id)
}
})
}
}
5 changes: 5 additions & 0 deletions gts/gts_uuid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ func TestToUUID_Instances(t *testing.T) {
gtsID: "gts.x.test5.events.type.v1~abc.app._.custom_event.v1.2",
expected: "c7f8cca7-3af6-58af-b72b-3febfd93f1a8",
},
{
name: "Combined anonymous instance (UUID tail)",
gtsID: "gts.x.core.events.type.v1~x.commerce.orders.order_placed.v1.0~7a1d2f34-5678-49ab-9012-abcdef123456",
expected: "4a31b759-722b-5bb1-a1dc-2cf40963e81b",
},
}

for _, tt := range tests {
Expand Down
3 changes: 3 additions & 0 deletions gts/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type ParseIDSegment struct {
VerMajor int `json:"ver_major"`
VerMinor *int `json:"ver_minor"`
IsType bool `json:"is_type"`
IsUUID bool `json:"is_uuid"`
}

// ParseIDResult represents the result of parsing a GTS identifier
Expand Down Expand Up @@ -58,6 +59,7 @@ func ParseID(gtsID string) ParseIDResult {
VerMajor: seg.VerMajor,
VerMinor: seg.VerMinor,
IsType: seg.IsType,
IsUUID: seg.IsUUID,
}
}

Expand Down Expand Up @@ -97,6 +99,7 @@ func ParseID(gtsID string) ParseIDResult {
VerMajor: seg.VerMajor,
VerMinor: seg.VerMinor,
IsType: seg.IsType,
IsUUID: seg.IsUUID,
}
}

Expand Down
Loading
Loading