typia 15.2.0 → 15.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/lib/internal/_stringLengthGte.d.ts +7 -3
  2. package/lib/internal/_stringLengthGte.js +9 -3
  3. package/lib/internal/_stringLengthGte.js.map +1 -1
  4. package/lib/internal/_stringLengthGte.mjs +8 -3
  5. package/lib/internal/_stringLengthGte.mjs.map +1 -1
  6. package/lib/internal/_stringLengthLte.d.ts +7 -3
  7. package/lib/internal/_stringLengthLte.js +9 -3
  8. package/lib/internal/_stringLengthLte.js.map +1 -1
  9. package/lib/internal/_stringLengthLte.mjs +8 -3
  10. package/lib/internal/_stringLengthLte.mjs.map +1 -1
  11. package/native/cmd/ttsc-typia/json_schema_recursive_array_alias_transform_test.go +79 -0
  12. package/native/core/context/ImportProgrammer.go +19 -9
  13. package/native/core/context/ImportProgrammerPattern.go +36 -0
  14. package/native/core/factories/internal/metadata/iterate_metadata_intersection.go +4 -1
  15. package/native/core/programmers/RandomProgrammer.go +19 -14
  16. package/native/core/programmers/internal/CheckerProgrammer.go +13 -6
  17. package/native/core/programmers/iterate/check_string.go +41 -5
  18. package/native/core/programmers/iterate/check_string_pattern_test.go +91 -0
  19. package/native/core/programmers/iterate/check_template.go +4 -4
  20. package/native/core/programmers/iterate/json_schema_alias.go +22 -24
  21. package/native/core/programmers/iterate/json_schema_array.go +14 -10
  22. package/native/core/programmers/iterate/json_schema_array_names.go +113 -0
  23. package/native/core/programmers/iterate/json_schema_native.go +10 -3
  24. package/native/core/programmers/iterate/json_schema_recursive_array_identity_test.go +75 -0
  25. package/native/core/programmers/iterate/json_schema_recursive_array_ownership_test.go +112 -0
  26. package/native/core/programmers/iterate/json_schema_station.go +5 -1
  27. package/native/core/programmers/json/JsonSchemasProgrammer.go +1 -0
  28. package/native/core/programmers/random_programmer_array_schema_reference_test.go +53 -0
  29. package/package.json +3 -3
  30. package/src/internal/_stringLengthGte.ts +8 -3
  31. package/src/internal/_stringLengthLte.ts +8 -3
@@ -0,0 +1,91 @@
1
+ package iterate
2
+
3
+ import (
4
+ "testing"
5
+
6
+ shimast "github.com/microsoft/typescript-go/shim/ast"
7
+ shimprinter "github.com/microsoft/typescript-go/shim/printer"
8
+ nativecontext "github.com/samchon/typia/packages/typia/native/core/context"
9
+ nativemetadata "github.com/samchon/typia/packages/typia/native/core/schemas/metadata"
10
+ )
11
+
12
+ // TestCheckStringCachesOnlyStaticPatterns verifies the string check's cache boundary.
13
+ //
14
+ // Pattern-kind metadata alone cannot justify replacing arbitrary validation
15
+ // code. Only a flag-free constructor with the authored source and input has
16
+ // the standard tag's stateless behavior.
17
+ //
18
+ // 1. Build real string-check expressions for standard and custom tag forms.
19
+ // 2. Require one lazy var slot for repeated standard sources in compiler mode.
20
+ // 3. Preserve inline checks without new names in standalone mode, and preserve
21
+ // custom expressions in both modes.
22
+ //
23
+ // @evidence contracts/testing.md#behavioral-verification Check_string builds AST checks from tag metadata. Standard and empty sources register one lazy uninitialized var after repeated compiler-integrated calls; flags, altered source, altered input, arbitrary predicates and other tag kinds register none. Standalone mode retains the inline constructor and emits no potentially conflicting name.
24
+ // @evidence contracts/testing.md#independent-expectations A flag-free RegExp(source).test(input) can reuse one stateless object. Additional flags or authored expressions are independent behavior and must remain unchanged; uninitialized var slots preserve lazy and early-module use.
25
+ // @evidence contracts/testing.md#distinguishing-cases Standard, empty and invalid sources have custom flags, mismatched source, mismatched input, custom predicate, non-pattern kind and absent importer negative twins. Two distinct sources require two slots; repeated requests reuse one. Standalone mode contrasts with compiler mode because it cannot safely allocate a name beside authored declarations.
26
+ // @evidence contracts/testing.md#execution-ownership The native Go suite discovers this in-process unit test. It calls the real checker builder and importer with in-memory AST and metadata; no compiler artifact or JavaScript subprocess is needed. The schema E2E case owns actual module lowering and construction counts.
27
+ func TestCheckStringCachesOnlyStaticPatterns(t *testing.T) {
28
+ for _, emit := range []*shimprinter.EmitContext{nil, shimprinter.NewEmitContext()} {
29
+ for _, row := range []struct {
30
+ source string
31
+ script string
32
+ kind string
33
+ cached bool
34
+ }{
35
+ {"x", `RegExp("x").test($input)`, "pattern", true},
36
+ {"", `RegExp("").test($input)`, "pattern", true},
37
+ {"[", `RegExp("[").test($input)`, "pattern", true},
38
+ {"x", `RegExp("x", "g").test($input)`, "pattern", false},
39
+ {"x", `RegExp("y").test($input)`, "pattern", false},
40
+ {"x", `RegExp("x").test("other")`, "pattern", false},
41
+ {"x", `$input === "allow"`, "pattern", false},
42
+ {"x", `RegExp("x").test($input)`, "custom", false},
43
+ } {
44
+ importer := nativecontext.NewImportProgrammer()
45
+ if emit != nil {
46
+ importer.SetEmitContext(emit)
47
+ }
48
+ props := Check_stringProps{
49
+ Context: nativecontext.ITypiaContext{Emit: emit, Importer: importer},
50
+ Input: nativecontext.NodeFactoryOf(emit).NewIdentifier("input"),
51
+ Atomic: &nativemetadata.MetadataAtomic{Type: "string", Tags: [][]nativemetadata.IMetadataTypeTag{{{
52
+ Target: "string", Name: "test", Kind: row.kind, Value: row.source, Validate: row.script,
53
+ }}}},
54
+ }
55
+ first := Check_string(props).Conditions[0][0].Expression
56
+ Check_string(props)
57
+ statements := importer.ToStatements()
58
+ expected := 0
59
+ if row.cached && emit != nil {
60
+ expected = 1
61
+ }
62
+ if len(statements) != expected {
63
+ t.Fatalf("%s: expected %d cache slots, got %d", row.script, expected, len(statements))
64
+ }
65
+ if row.cached && emit == nil {
66
+ receiver := first.AsCallExpression().Expression.AsPropertyAccessExpression().Expression
67
+ if receiver.Kind != shimast.KindCallExpression {
68
+ t.Fatal("standalone mode must preserve the inline constructor without a new binding")
69
+ }
70
+ }
71
+ if expected != 0 {
72
+ declaration := statements[0].AsVariableStatement().DeclarationList.AsVariableDeclarationList()
73
+ if declaration.Flags&(shimast.NodeFlagsLet|shimast.NodeFlagsConst) != 0 || declaration.Declarations.Nodes[0].AsVariableDeclaration().Initializer != nil {
74
+ t.Fatalf("%s: cache must use an uninitialized var", row.script)
75
+ }
76
+ receiver := first.AsCallExpression().Expression.AsPropertyAccessExpression().Expression
77
+ if receiver.Kind != shimast.KindParenthesizedExpression || receiver.AsParenthesizedExpression().Expression.AsBinaryExpression().OperatorToken.Kind != shimast.KindQuestionQuestionEqualsToken {
78
+ t.Fatalf("%s: construction must be lazy", row.script)
79
+ }
80
+ props.Atomic.Tags[0][0].Value = "different"
81
+ props.Atomic.Tags[0][0].Validate = `RegExp("different").test($input)`
82
+ Check_string(props)
83
+ if len(importer.ToStatements()) != 2 {
84
+ t.Fatal("distinct sources must have distinct slots")
85
+ }
86
+ }
87
+ props.Context.Importer = nil
88
+ Check_string(props)
89
+ }
90
+ }
91
+ }
@@ -30,7 +30,7 @@ type Check_templateProps struct {
30
30
  // guard plus one `RegExp(...).test` per template, with the type tags inlined
31
31
  // into the member when several templates are neighbors.
32
32
  //
33
- // @evidence contracts/common.md#principled-implementation It builds the check entry of template literal types: a string guard plus one `RegExp(...).test` per template, with the type tags inlined into the member when several templates are neighbors.
33
+ // @evidence contracts/common.md#principled-implementation It builds the check entry of template literal types: a string guard plus one structural regex per template. Whole-string and captured-placeholder tag conditions use the common string-tag lowering, including backtracking and union members, so standard Pattern tags share the file cache without changing structural matching.
34
34
  // @evidence contracts/common.md#clear-and-simple-design One exported function; the pieces that repeat live in private helpers of the same file.
35
35
  // @evidence contracts/common.md#prohibited-implementation-shortcuts Its inputs are its arguments and the context they carry, and it keeps no state of its own.
36
36
  // @evidence contracts/common.md#meaningful-documentation The doc states what it builds.
@@ -125,7 +125,7 @@ func check_template_type_tags(props Check_templateProps, tpl *nativemetadata.Met
125
125
  for _, tag := range tags {
126
126
  conditions = append(conditions, nativehelpers.ICheckEntry_ICondition{
127
127
  Expected: tpl.GetBaseName() + " & " + tag.Name,
128
- Expression: check_string_transpile(props.Context, tag.Validate)(props.Input),
128
+ Expression: check_string_transpile(props.Context, tag)(props.Input),
129
129
  })
130
130
  }
131
131
  for _, tag := range row {
@@ -253,7 +253,7 @@ func check_template_value_conditions(props Check_templateProps, base string, ato
253
253
  for _, tag := range check_string_filter_validate(row) {
254
254
  conditions = append(conditions, nativehelpers.ICheckEntry_ICondition{
255
255
  Expected: base + " & " + tag.Name,
256
- Expression: check_string_transpile(props.Context, tag.Validate)(input),
256
+ Expression: check_string_transpile(props.Context, tag)(input),
257
257
  })
258
258
  }
259
259
  for _, tag := range row {
@@ -611,7 +611,7 @@ func check_template_inline_tags(props Check_templateProps, tpl *nativemetadata.M
611
611
  tags := check_string_filter_validate(row)
612
612
  expressions := make([]*shimast.Node, 0, len(tags))
613
613
  for _, tag := range tags {
614
- expressions = append(expressions, check_string_transpile(props.Context, tag.Validate)(props.Input))
614
+ expressions = append(expressions, check_string_transpile(props.Context, tag)(props.Input))
615
615
  }
616
616
  for _, tag := range row {
617
617
  if condition := check_exclude_condition("string", tag, props.Input, props.Emit); condition != nil {
@@ -25,38 +25,36 @@ func json_schema_alias(props struct {
25
25
  props.components.emplaceSchemaKey(props.alias.Type.Name)
26
26
  props.components.Schemas[props.alias.Type.Name] = JsonSchema{}
27
27
 
28
- attribute := JsonSchema{}
29
- if json_schema_has_tag(props.alias.Type.JsDocTags, "deprecated") {
30
- attribute["deprecated"] = true
31
- }
32
- if title := json_schema_title(struct {
33
- description *string
34
- jsDocTags []nativemetadata.IJsDocTagInfo
35
- }{
36
- description: props.alias.Type.Description,
37
- jsDocTags: props.alias.Type.JsDocTags,
38
- }); title != nil {
39
- attribute["title"] = *title
40
- }
41
- if description := json_schema_description(struct {
42
- description *string
43
- jsDocTags []nativemetadata.IJsDocTagInfo
44
- }{
45
- description: props.alias.Type.Description,
46
- jsDocTags: props.alias.Type.JsDocTags,
47
- }); description != nil {
48
- attribute["description"] = *description
49
- }
50
28
  schema := json_schema_station(json_schema_station_props{
51
29
  blockNever: props.blockNever,
52
30
  components: props.components,
53
- attribute: attribute,
31
+ attribute: JsonSchema{},
54
32
  metadata: props.alias.Type.Value,
33
+ definition: props.alias.Type.Name,
55
34
  })
56
35
  if schema != nil {
57
- json_schema_jsDocTags(schema, props.alias.Type.JsDocTags)
58
36
  json_schema_assign(props.components.Schemas[props.alias.Type.Name], schema)
59
37
  }
60
38
  }
39
+ // An array may reserve this component before encountering its alias through
40
+ // an item. Apply alias annotations even to that in-progress definition; the
41
+ // structural owner subsequently fills the same map without losing them.
42
+ target := props.components.Schemas[props.alias.Type.Name]
43
+ if json_schema_has_tag(props.alias.Type.JsDocTags, "deprecated") {
44
+ target["deprecated"] = true
45
+ }
46
+ if title := json_schema_title(struct {
47
+ description *string
48
+ jsDocTags []nativemetadata.IJsDocTagInfo
49
+ }{description: props.alias.Type.Description, jsDocTags: props.alias.Type.JsDocTags}); title != nil {
50
+ target["title"] = *title
51
+ }
52
+ if description := json_schema_description(struct {
53
+ description *string
54
+ jsDocTags []nativemetadata.IJsDocTagInfo
55
+ }{description: props.alias.Type.Description, jsDocTags: props.alias.Type.JsDocTags}); description != nil {
56
+ target["description"] = *description
57
+ }
58
+ json_schema_jsDocTags(target, props.alias.Type.JsDocTags)
61
59
  return []JsonSchema{{"$ref": ref}}
62
60
  }
@@ -17,6 +17,7 @@ type Json_schema_array_export_props struct {
17
17
  func json_schema_array(props struct {
18
18
  components *OpenApi_IComponents
19
19
  array *nativemetadata.MetadataArray
20
+ definition string
20
21
  }) []JsonSchema {
21
22
  factory := func() []JsonSchema {
22
23
  return json_schema_plugin(struct {
@@ -35,27 +36,28 @@ func json_schema_array(props struct {
35
36
  tags: props.array.Tags,
36
37
  })
37
38
  }
38
- if props.array.Type.Recursive {
39
+ key := props.components.arraySchemaName(props.array)
40
+ if props.array.Type.Recursive && key != props.definition {
39
41
  out := func() []JsonSchema {
40
- return []JsonSchema{{"$ref": "#/components/schemas/" + props.array.Type.Name}}
42
+ return []JsonSchema{{"$ref": "#/components/schemas/" + key}}
41
43
  }
42
44
  if props.components.Schemas != nil {
43
- if _, ok := props.components.Schemas[props.array.Type.Name]; ok {
45
+ if _, ok := props.components.Schemas[key]; ok {
44
46
  return out()
45
47
  }
46
48
  }
47
49
  if props.components.Schemas == nil {
48
50
  props.components.Schemas = map[string]JsonSchema{}
49
51
  }
50
- if _, ok := props.components.Schemas[props.array.Type.Name]; ok == false {
51
- props.components.emplaceSchemaKey(props.array.Type.Name)
52
- props.components.Schemas[props.array.Type.Name] = JsonSchema{}
52
+ if _, ok := props.components.Schemas[key]; ok == false {
53
+ props.components.emplaceSchemaKey(key)
54
+ props.components.Schemas[key] = JsonSchema{}
53
55
  }
54
56
  oneOf := factory()
55
57
  if len(oneOf) == 1 {
56
- json_schema_assign(props.components.Schemas[props.array.Type.Name], oneOf[0])
58
+ json_schema_assign(props.components.Schemas[key], oneOf[0])
57
59
  } else {
58
- props.components.Schemas[props.array.Type.Name]["oneOf"] = oneOf
60
+ props.components.Schemas[key]["oneOf"] = oneOf
59
61
  }
60
62
  return out()
61
63
  }
@@ -65,9 +67,10 @@ func json_schema_array(props struct {
65
67
  // Json_schema_array_export converts an array type to its JSON schemas: an
66
68
  // `array` schema whose items come from the element metadata, or a reference to
67
69
  // the component of a recursive array type, which is registered the first time it
68
- // is met.
70
+ // is met. Recursive uses with different schema tags get distinct components,
71
+ // so a length constraint cannot leak into another use of the same array type.
69
72
  //
70
- // @evidence contracts/common.md#principled-implementation It converts an array type to its JSON schemas: an `array` schema whose items come from the element metadata, or a reference to the component of a recursive array type, which is registered the first time it is met.
73
+ // @evidence contracts/common.md#principled-implementation A recursive array reserves its component before descending into items. Schema-bearing tags participate in component identity, and a same-named alias body is built inline instead of becoming its own reference. Nested stations retain references, terminating recursive expansion.
71
74
  // @evidence contracts/common.md#clear-and-simple-design One exported function; the pieces that repeat live in private helpers of the same file.
72
75
  // @evidence contracts/common.md#prohibited-implementation-shortcuts The only state it changes is the components record it is given.
73
76
  // @evidence contracts/common.md#meaningful-documentation The doc states what it builds.
@@ -75,6 +78,7 @@ func Json_schema_array_export(props Json_schema_array_export_props) []JsonSchema
75
78
  return json_schema_array(struct {
76
79
  components *OpenApi_IComponents
77
80
  array *nativemetadata.MetadataArray
81
+ definition string
78
82
  }{
79
83
  components: props.Components,
80
84
  array: props.Array,
@@ -0,0 +1,113 @@
1
+ package iterate
2
+
3
+ import (
4
+ "encoding/json"
5
+ "strconv"
6
+
7
+ metadata "github.com/samchon/typia/packages/typia/native/core/schemas/metadata"
8
+ )
9
+
10
+ // ReserveSchemaNames reserves named definitions reachable from every root before
11
+ // generated array-variant names are allocated. Callers sharing components across
12
+ // several roots must supply all roots before generating any of them.
13
+ //
14
+ // @evidence contracts/common.md#principled-implementation A visited-set graph walk reserves aliases, arrays, tuples, objects and native names before variant allocation. Reserving all roots prevents a generated variant from taking a later declaration's name, including through recursive or shared edges.
15
+ // @evidence contracts/common.md#clear-and-simple-design Component-local maps own both visited metadata and reserved names; the walk follows only edges consumed by schema generation.
16
+ // @evidence contracts/common.md#prohibited-implementation-shortcuts Reservations use metadata identity and names, without fixture names or foreign mutations.
17
+ // @evidence contracts/common.md#meaningful-documentation The comment states why reservations precede generation and the obligation for multiple roots.
18
+ func (components *OpenApi_IComponents) ReserveSchemaNames(roots []*metadata.MetadataSchema) {
19
+ if components.reservedSchemaNames == nil {
20
+ components.reservedSchemaNames = map[string]bool{}
21
+ components.reservedMetadata = map[*metadata.MetadataSchema]bool{}
22
+ }
23
+ var visit func(*metadata.MetadataSchema)
24
+ visit = func(schema *metadata.MetadataSchema) {
25
+ if schema == nil || components.reservedMetadata[schema] {
26
+ return
27
+ }
28
+ components.reservedMetadata[schema] = true
29
+ for _, alias := range schema.Aliases {
30
+ components.reservedSchemaNames[alias.Type.Name] = true
31
+ visit(alias.Type.Value)
32
+ }
33
+ for _, array := range schema.Arrays {
34
+ components.reservedSchemaNames[array.Type.Name] = true
35
+ visit(array.Type.Value)
36
+ }
37
+ for _, tuple := range schema.Tuples {
38
+ components.reservedSchemaNames[tuple.Type.Name] = true
39
+ for _, element := range tuple.Type.Elements {
40
+ visit(element)
41
+ }
42
+ }
43
+ for _, object := range schema.Objects {
44
+ components.reservedSchemaNames[object.Type.Name] = true
45
+ for _, property := range object.Type.Properties {
46
+ visit(property.Value)
47
+ }
48
+ }
49
+ for _, native := range schema.Natives {
50
+ components.reservedSchemaNames[native.Name] = true
51
+ }
52
+ if schema.Escaped != nil {
53
+ visit(schema.Escaped.Returns)
54
+ }
55
+ visit(schema.Rest)
56
+ }
57
+ for _, root := range roots {
58
+ visit(root)
59
+ }
60
+ }
61
+
62
+ // arraySchemaName distinguishes recursive uses by the JSON schema tag rows that
63
+ // actually affect emission. JSON map encoding is key-sorted, so equivalent schema
64
+ // maps share an identity independently of their Go insertion order.
65
+ func (components *OpenApi_IComponents) arraySchemaName(array *metadata.MetadataArray) string {
66
+ if !array.Type.Recursive {
67
+ return array.Type.Name
68
+ }
69
+ rows := [][]any{}
70
+ for _, row := range array.Tags {
71
+ schemas := []any{}
72
+ for _, tag := range row {
73
+ if tag.Schema != nil {
74
+ schemas = append(schemas, tag.Schema)
75
+ }
76
+ }
77
+ if len(schemas) != 0 {
78
+ rows = append(rows, schemas)
79
+ }
80
+ }
81
+ if len(rows) == 0 {
82
+ return array.Type.Name
83
+ }
84
+ encoded, err := json.Marshal(rows)
85
+ if err != nil {
86
+ panic("array schema tags must be JSON serializable: " + err.Error())
87
+ }
88
+ identity := array.Type.Name + "\x00" + string(encoded)
89
+ if key, ok := components.arraySchemaNames[identity]; ok {
90
+ return key
91
+ }
92
+ components.ReserveSchemaNames([]*metadata.MetadataSchema{{Arrays: []*metadata.MetadataArray{array}}})
93
+ base := array.Type.Name + "-array"
94
+ index := components.arraySchemaCounters[base]
95
+ key := base
96
+ if index != 0 {
97
+ key = base + "-" + strconv.Itoa(index)
98
+ }
99
+ for components.reservedSchemaNames[key] || components.Schemas[key] != nil {
100
+ index++
101
+ key = base + "-" + strconv.Itoa(index)
102
+ }
103
+ components.reservedSchemaNames[key] = true
104
+ if components.arraySchemaNames == nil {
105
+ components.arraySchemaNames = map[string]string{}
106
+ }
107
+ if components.arraySchemaCounters == nil {
108
+ components.arraySchemaCounters = map[string]int{}
109
+ }
110
+ components.arraySchemaNames[identity] = key
111
+ components.arraySchemaCounters[base] = index + 1
112
+ return key
113
+ }
@@ -6,10 +6,11 @@ import (
6
6
  )
7
7
 
8
8
  // OpenApi_IComponents is the `components` of a JSON schema document being built:
9
- // the named schemas and the order in which they were first registered.
9
+ // the named schemas, their discovery order and the name reservations used by
10
+ // tagged recursive array variants. All state belongs to one generated document.
10
11
  //
11
- // @evidence contracts/common.md#principled-implementation It is the `components` of a JSON schema document being built: the named schemas and the order in which they were first registered; its 2 fields (Schemas, Order) are named so that a producer and a consumer cannot transpose them.
12
- // @evidence contracts/common.md#clear-and-simple-design The schema map owns lookup while Order owns discovery sequence; registration and literal rendering are methods on that shared store.
12
+ // @evidence contracts/common.md#principled-implementation Schemas owns component bodies and Order preserves registration sequence. Private reservations prevent generated array variants from colliding with declared names; their identity map distinguishes schema-bearing tag rows within one document.
13
+ // @evidence contracts/common.md#clear-and-simple-design Public schema data stays separate from private allocation state. Registration, name reservation and literal rendering belong to the shared document store.
13
14
  // @evidence contracts/common.md#prohibited-implementation-shortcuts Registration records new keys before insertion and rendering follows the recorded sequence rather than relying on Go map iteration.
14
15
  // @evidence contracts/common.md#meaningful-documentation The doc states what the record is.
15
16
  type OpenApi_IComponents struct {
@@ -20,6 +21,12 @@ type OpenApi_IComponents struct {
20
21
  // collection was a plain object keyed in insertion order. A Go map is
21
22
  // unordered, so the bare map alone would scramble the keys.
22
23
  Order []string
24
+
25
+ // Private allocation state is discarded with this document after emission.
26
+ reservedSchemaNames map[string]bool
27
+ reservedMetadata map[*nativemetadata.MetadataSchema]bool
28
+ arraySchemaNames map[string]string
29
+ arraySchemaCounters map[string]int
23
30
  }
24
31
 
25
32
  // ToLiteral renders the components as an order-preserving object literal input
@@ -0,0 +1,75 @@
1
+ package iterate
2
+
3
+ import (
4
+ "reflect"
5
+ "testing"
6
+
7
+ metadata "github.com/samchon/typia/packages/typia/native/core/schemas/metadata"
8
+ )
9
+
10
+ // TestJsonSchemaRecursiveArrayIdentity verifies variant names cannot capture
11
+ // later declared names and recursive tagged edges reach their own definition.
12
+ //
13
+ // Type names alone cannot distinguish constraints, while generated names must
14
+ // coexist with every declared root regardless of its traversal order.
15
+ //
16
+ // 1. Reserve a later alias with the preferred generated variant name.
17
+ // 2. Build self-tagged arrays with equivalent and different schema tag maps.
18
+ // 3. Check reuse, separation, recursive targets and the untouched later alias.
19
+ //
20
+ // @evidence contracts/testing.md#behavioral-verification Exercises the array component allocator and writer on a cyclic tagged graph; checks name collision avoidance, exact tagged self-reference, equivalent-map reuse and constraint separation.
21
+ // @evidence contracts/testing.md#independent-expectations A tagged recursive element must reference the same constrained component at every depth. A separately declared string alias must remain a string even if its name matches a generated candidate.
22
+ // @evidence contracts/testing.md#distinguishing-cases Equivalent maps in opposite insertion order share a component, different constraints do not, a later declared name wins its reservation and an untagged component remains separate.
23
+ // @evidence contracts/testing.md#execution-ownership pnpm test:go:native discovers this unit test and executes the owning writer in process with authored metadata and no product subprocess.
24
+ func TestJsonSchemaRecursiveArrayIdentity(t *testing.T) {
25
+ value := metadata.MetadataSchema_initialize()
26
+ array := &metadata.MetadataArrayType{Name: "Tree", Recursive: true, Value: value}
27
+ first := map[string]any{"minItems": 1, "maxItems": 3}
28
+ second := map[string]any{}
29
+ second["maxItems"] = 3
30
+ second["minItems"] = 1
31
+ tagged := &metadata.MetadataArray{Type: array, Tags: [][]metadata.IMetadataTypeTag{{{Schema: first}}}}
32
+ value.Arrays = []*metadata.MetadataArray{tagged}
33
+ root := metadata.MetadataSchema_initialize()
34
+ root.Arrays = []*metadata.MetadataArray{tagged}
35
+ later := metadata.MetadataSchema_initialize()
36
+ later.Aliases = []*metadata.MetadataAlias{{Type: &metadata.MetadataAliasType{
37
+ Name: "Tree-array", Value: &metadata.MetadataSchema{Atomics: []*metadata.MetadataAtomic{{Type: "string"}}},
38
+ }}}
39
+ components := &OpenApi_IComponents{}
40
+ components.ReserveSchemaNames([]*metadata.MetadataSchema{root, later})
41
+ output := Json_schema_station(Json_schema_station_props{Components: components, Metadata: root})
42
+ ref := output["$ref"].(string)
43
+ key := ref[len("#/components/schemas/"):]
44
+ if key == "Tree-array" {
45
+ t.Fatal("variant captured a later declaration")
46
+ }
47
+ expected := JsonSchema{"type": "array", "minItems": 1, "maxItems": 3, "items": JsonSchema{"$ref": ref}}
48
+ if !reflect.DeepEqual(components.Schemas[key], expected) {
49
+ t.Errorf("recursive tagged body: %#v", components.Schemas[key])
50
+ }
51
+ equivalent := Json_schema_array_export(Json_schema_array_export_props{Components: components, Array: &metadata.MetadataArray{
52
+ Type: array, Tags: [][]metadata.IMetadataTypeTag{{{Schema: second}}},
53
+ }})
54
+ if !reflect.DeepEqual(equivalent, []JsonSchema{output}) {
55
+ t.Errorf("equivalent tags changed identity: %#v", equivalent)
56
+ }
57
+ other := Json_schema_array_export(Json_schema_array_export_props{Components: components, Array: &metadata.MetadataArray{
58
+ Type: array, Tags: [][]metadata.IMetadataTypeTag{{{Schema: map[string]any{"minItems": 2}}}},
59
+ }})
60
+ if reflect.DeepEqual(other, equivalent) {
61
+ t.Fatal("different constraints shared one identity")
62
+ }
63
+ Json_schema_station(Json_schema_station_props{Components: components, Metadata: later})
64
+ if !reflect.DeepEqual(components.Schemas["Tree-array"], JsonSchema{"type": "string"}) {
65
+ t.Errorf("later alias was overwritten: %#v", components.Schemas["Tree-array"])
66
+ }
67
+ plain := Json_schema_array_export(Json_schema_array_export_props{Components: components, Array: &metadata.MetadataArray{Type: array}})
68
+ if !reflect.DeepEqual(plain, []JsonSchema{{"$ref": "#/components/schemas/Tree"}}) {
69
+ t.Errorf("plain identity changed: %#v", plain)
70
+ }
71
+ expectedPlain := JsonSchema{"type": "array", "items": JsonSchema{"$ref": ref}}
72
+ if !reflect.DeepEqual(components.Schemas["Tree"], expectedPlain) {
73
+ t.Errorf("plain use inherited tags: %#v", components.Schemas["Tree"])
74
+ }
75
+ }
@@ -0,0 +1,112 @@
1
+ package iterate
2
+
3
+ import (
4
+ "reflect"
5
+ "testing"
6
+
7
+ metadata "github.com/samchon/typia/packages/typia/native/core/schemas/metadata"
8
+ )
9
+
10
+ // TestJsonSchemaRecursiveArrayOwnership verifies aliases define their array body
11
+ // and array constraints belong to each use rather than the shared component.
12
+ //
13
+ // A reserved component is a recursion anchor, not evidence that its structural
14
+ // body has already been generated. Array tags describe a use of that anchor.
15
+ //
16
+ // 1. Generate an alias and its same-named recursive array in both visit orders.
17
+ // 2. Generate two differently tagged uses of a shared recursive array.
18
+ // 3. Compare complete definitions and references with authored array semantics.
19
+ //
20
+ // @evidence contracts/testing.md#behavioral-verification Calls the schema station on cyclic metadata and compares complete component bodies, returned references, tag rows and registration order.
21
+ // @evidence contracts/testing.md#independent-expectations An array definition has type array and an items schema; a recursive reference alone cannot define it. Min/max item constraints are per-use intersections, independently of traversal order.
22
+ // @evidence contracts/testing.md#distinguishing-cases Alias-first and array-first roots retain the alias description; tagged and untagged uses, one and multiple tag rows and nonrecursive controls are separate subcases. Nullable and distinct-name declarations are covered by the TypeScript producer matrix.
23
+ // @evidence contracts/testing.md#execution-ownership pnpm test:go:native discovers this in-process unit test; constructed metadata exercises the owning schema writer without a compiler or subprocess.
24
+ func TestJsonSchemaRecursiveArrayOwnership(t *testing.T) {
25
+ for _, arrayFirst := range []bool{false, true} {
26
+ t.Run(map[bool]string{false: "alias-first", true: "array-first"}[arrayFirst], func(t *testing.T) {
27
+ description := "Recursive tree."
28
+ alias := &metadata.MetadataAliasType{Name: "Tree", Description: &description}
29
+ reference := metadata.MetadataSchema_initialize()
30
+ reference.Aliases = []*metadata.MetadataAlias{{Type: alias}}
31
+ array := &metadata.MetadataArrayType{Name: "Tree", Recursive: true, Value: reference}
32
+ body := metadata.MetadataSchema_initialize()
33
+ body.Arrays = []*metadata.MetadataArray{{Type: array}}
34
+ alias.Value = body
35
+ components := &OpenApi_IComponents{}
36
+ roots := []*metadata.MetadataSchema{reference, body}
37
+ if arrayFirst {
38
+ roots[0], roots[1] = roots[1], roots[0]
39
+ }
40
+ for _, root := range roots {
41
+ actual := Json_schema_station(Json_schema_station_props{Components: components, Metadata: root})
42
+ if !reflect.DeepEqual(actual, JsonSchema{"$ref": "#/components/schemas/Tree"}) {
43
+ t.Errorf("root: %#v", actual)
44
+ }
45
+ }
46
+ expected := JsonSchema{"type": "array", "items": JsonSchema{"$ref": "#/components/schemas/Tree"}, "description": description}
47
+ if !reflect.DeepEqual(components.Schemas["Tree"], expected) {
48
+ t.Errorf("definition: %#v, want %#v", components.Schemas["Tree"], expected)
49
+ }
50
+ if !reflect.DeepEqual(components.Order, []string{"Tree"}) {
51
+ t.Errorf("registration order: %#v", components.Order)
52
+ }
53
+ })
54
+ }
55
+ for _, recursive := range []bool{false, true} {
56
+ t.Run(map[bool]string{false: "inline-tags", true: "recursive-tags"}[recursive], func(t *testing.T) {
57
+ value := metadata.MetadataSchema_initialize()
58
+ array := &metadata.MetadataArrayType{Name: "Nested", Recursive: recursive, Value: value}
59
+ if recursive {
60
+ value.Arrays = []*metadata.MetadataArray{{Type: array}}
61
+ } else {
62
+ value.Atomics = []*metadata.MetadataAtomic{{Type: "string"}}
63
+ }
64
+ components := &OpenApi_IComponents{}
65
+ for _, sample := range []struct {
66
+ tags [][]metadata.IMetadataTypeTag
67
+ constraints []JsonSchema
68
+ }{
69
+ {[][]metadata.IMetadataTypeTag{{{Schema: map[string]any{"minItems": 1}}}}, []JsonSchema{{"minItems": 1}}},
70
+ {[][]metadata.IMetadataTypeTag{{{Schema: map[string]any{"maxItems": 2}}}, {{Schema: map[string]any{"minItems": 3}}}}, []JsonSchema{{"maxItems": 2}, {"minItems": 3}}},
71
+ {nil, []JsonSchema{{}}},
72
+ } {
73
+ actual := Json_schema_array_export(Json_schema_array_export_props{Components: components, Array: &metadata.MetadataArray{Type: array, Tags: sample.tags}})
74
+ base := JsonSchema{"type": "array", "items": JsonSchema{"type": "string"}}
75
+ if recursive {
76
+ if len(actual) != 1 {
77
+ t.Fatalf("expected one component reference: %#v", actual)
78
+ }
79
+ ref, ok := actual[0]["$ref"].(string)
80
+ if !ok {
81
+ t.Fatalf("expected a component reference: %#v", actual)
82
+ }
83
+ base = JsonSchema{"type": "array", "items": JsonSchema{"$ref": "#/components/schemas/Nested"}}
84
+ key := ref[len("#/components/schemas/"):]
85
+ actual = []JsonSchema{components.Schemas[key]}
86
+ if branches, ok := actual[0]["oneOf"].([]JsonSchema); ok {
87
+ actual = branches
88
+ }
89
+ }
90
+ expected := []JsonSchema{}
91
+ for _, constraint := range sample.constraints {
92
+ schema := cloneJsonSchema(base)
93
+ for key, value := range constraint {
94
+ schema[key] = value
95
+ }
96
+ expected = append(expected, schema)
97
+ }
98
+ if !reflect.DeepEqual(actual, expected) {
99
+ t.Errorf("uses: %#v, want %#v", actual, expected)
100
+ }
101
+ }
102
+ if recursive {
103
+ expected := JsonSchema{"type": "array", "items": JsonSchema{"$ref": "#/components/schemas/Nested"}}
104
+ if !reflect.DeepEqual(components.Schemas["Nested"], expected) {
105
+ t.Errorf("tags polluted definition: %#v", components.Schemas["Nested"])
106
+ }
107
+ } else if len(components.Schemas) != 0 {
108
+ t.Errorf("nonrecursive array registered components: %#v", components.Schemas)
109
+ }
110
+ })
111
+ }
112
+ }
@@ -10,6 +10,9 @@ type json_schema_station_props struct {
10
10
  components *OpenApi_IComponents
11
11
  attribute JsonSchema
12
12
  metadata *nativemetadata.MetadataSchema
13
+ // definition is the alias component whose body this station is filling.
14
+ // Only this level may inline a same-named array; nested stations use refs.
15
+ definition string
13
16
  }
14
17
 
15
18
  // Json_schema_station_props is the argument record of Json_schema_station, which
@@ -110,7 +113,8 @@ func json_schema_station(props json_schema_station_props) JsonSchema {
110
113
  for _, schema := range json_schema_array(struct {
111
114
  components *OpenApi_IComponents
112
115
  array *nativemetadata.MetadataArray
113
- }{components: props.components, array: array}) {
116
+ definition string
117
+ }{components: props.components, array: array, definition: props.definition}) {
114
118
  insert(schema)
115
119
  }
116
120
  }
@@ -141,6 +141,7 @@ func jsonSchemasProgrammer_writeV3_1(metadataList []*nativemetadata.MetadataSche
141
141
  components := &nativeiterate.OpenApi_IComponents{
142
142
  Schemas: map[string]nativeiterate.JsonSchema{},
143
143
  }
144
+ components.ReserveSchemaNames(metadataList)
144
145
  generator := func(metadata *nativemetadata.MetadataSchema) nativeiterate.JsonSchema {
145
146
  return nativeiterate.Json_schema_station(nativeiterate.Json_schema_station_props{
146
147
  BlockNever: true,