typia 15.2.0 → 15.2.1

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.
@@ -0,0 +1,79 @@
1
+ package main
2
+
3
+ import (
4
+ "os"
5
+ "path/filepath"
6
+ "testing"
7
+ )
8
+
9
+ // TestJsonSchemaRecursiveArrayAliasTransform verifies recursive arrays survive
10
+ // analysis and produce finite, repeatable schema literals.
11
+ //
12
+ // The alias and array metadata may share an identity; constructed metadata alone
13
+ // cannot establish which names the TypeScript checker actually supplies.
14
+ //
15
+ // 1. Transform recursive, constrained and nonrecursive declarations together.
16
+ // 2. Compare named structural bodies and distinct use-site constraints.
17
+ // 3. Repeat emission and require identical complete JavaScript bytes.
18
+ //
19
+ // @evidence contracts/testing.md#behavioral-verification Runs the native transform in process, decodes emitted schema literals and compares array bodies and tagged properties; complete output equality pins deterministic emission.
20
+ // @evidence contracts/testing.md#independent-expectations The authored recursive type requires an array whose items reference the value union. Explicit max/min tags constrain different properties and must not modify an untagged sibling.
21
+ // @evidence contracts/testing.md#distinguishing-cases Union-root and array-root traversal, two tagged sibling uses and an untagged sibling coexist with an ordinary array control; emitted identity and constraint placement are asserted separately.
22
+ // @evidence contracts/testing.md#execution-ownership pnpm test:go:native discovers this test; runTransform and AST decoding execute in process using one fixture project, without compiler or JavaScript subprocesses.
23
+ func TestJsonSchemaRecursiveArrayAliasTransform(t *testing.T) {
24
+ dir := ttscTypiaTestFixtureDirectory(t, "recursive-array-schema-")
25
+ if err := os.MkdirAll(filepath.Join(dir, "src"), 0o755); err != nil {
26
+ t.Fatal(err)
27
+ }
28
+ for name, source := range map[string]string{
29
+ "tsconfig.json": atomicIntersectionSchemaTSConfig,
30
+ "src/main.ts": `import typia, { tags } from "typia";
31
+ type JsonArray = JsonValue[];
32
+ type JsonObject = { [key: string]: JsonValue };
33
+ type JsonValue = string | number | boolean | null | JsonArray | JsonObject;
34
+ interface Constrained {
35
+ small: JsonArray & tags.MaxItems<1>;
36
+ many: JsonArray & tags.MinItems<2>;
37
+ plain: JsonArray;
38
+ }
39
+ export const schemas = typia.json.schemas<[JsonValue, JsonArray, Constrained, string[]]>();
40
+ `,
41
+ } {
42
+ if err := os.WriteFile(filepath.Join(dir, filepath.FromSlash(name)), []byte(source), 0o644); err != nil {
43
+ t.Fatal(err)
44
+ }
45
+ }
46
+ var previous string
47
+ for i := 0; i < 3; i++ {
48
+ output, stderr, code := ttscTypiaTestCapture(func() int {
49
+ return runTransform([]string{"--cwd", dir, "--tsconfig", "tsconfig.json", "--file", "src/main.ts", "--output", "js"})
50
+ })
51
+ if code != 0 {
52
+ t.Fatalf("transform failed (%d): %s", code, stderr)
53
+ }
54
+ unit := ttscTypiaTestSchemaLiteral(t, output)
55
+ if i != 0 && output != previous {
56
+ t.Error("unchanged source produced different bytes")
57
+ }
58
+ previous = output
59
+ schemas := ttscTypiaTestSchemaPath(t, unit, "components", "schemas").(map[string]any)
60
+ body := schemas["JsonArray"].(map[string]any)
61
+ ttscTypiaTestSchemaEqual(t, body, map[string]any{"type": "array", "items": map[string]any{"$ref": "#/components/schemas/JsonValue"}})
62
+ properties := ttscTypiaTestSchemaPath(t, unit, "components", "schemas", "Constrained", "properties").(map[string]any)
63
+ for _, sample := range []struct {
64
+ name string
65
+ constraint string
66
+ value any
67
+ }{
68
+ {"small", "maxItems", float64(1)}, {"many", "minItems", float64(2)}, {"plain", "", nil},
69
+ } {
70
+ ref := properties[sample.name].(map[string]any)["$ref"].(string)
71
+ target := schemas[ref[len("#/components/schemas/"):]].(map[string]any)
72
+ expected := map[string]any{"type": "array", "items": map[string]any{"$ref": "#/components/schemas/JsonValue"}}
73
+ if sample.constraint != "" {
74
+ expected[sample.constraint] = sample.value
75
+ }
76
+ ttscTypiaTestSchemaEqual(t, target, expected)
77
+ }
78
+ }
79
+ }
@@ -327,7 +327,10 @@ func Iterate_metadata_intersection(props IMetadataIteratorProps) bool {
327
327
  options := props.Options
328
328
  options.Functional = false
329
329
  explore := props.Explore
330
- explore.Aliased = false
330
+ // Attach tags to the surviving structure, not to a new alias wrapper whose
331
+ // array/object bucket would be empty here. Nested members clear Aliased again
332
+ // when they descend, so recursive references still retain their aliases.
333
+ explore.Aliased = true
331
334
  explore.Escaped = false
332
335
  // Re-explore the surviving base alone (`Intersected: true` short-circuits the
333
336
  // intersection handler at the top of this function). TypeScript flattens nested
@@ -806,7 +806,7 @@ func randomProgrammer_decode_array(props randomProgrammer_decodeArrayProps) []*s
806
806
  Components: components,
807
807
  Array: props.Array,
808
808
  })
809
- schemaList = randomProgrammer_array_schema_list(props.Array, components, schemaList)
809
+ schemaList = randomProgrammer_array_schema_list(components, schemaList)
810
810
  f := nativecontext.NodeFactoryOf(props.Context.Emit)
811
811
  output := make([]*shimast.Node, 0, len(schemaList))
812
812
  if props.Array.Type.Recursive {
@@ -1414,25 +1414,30 @@ func randomProgrammer_schema_without_items(schema nativeiterate.JsonSchema, emit
1414
1414
  }
1415
1415
 
1416
1416
  func randomProgrammer_array_schema_list(
1417
- array *schemametadata.MetadataArray,
1418
1417
  components *nativeiterate.OpenApi_IComponents,
1419
1418
  schemaList []nativeiterate.JsonSchema,
1420
1419
  ) []nativeiterate.JsonSchema {
1421
- if array == nil ||
1422
- array.Type == nil ||
1423
- array.Type.Recursive == false ||
1424
- components == nil ||
1425
- components.Schemas == nil {
1420
+ if components == nil || components.Schemas == nil {
1426
1421
  return schemaList
1427
1422
  }
1428
- schema, ok := components.Schemas[array.Type.Name]
1429
- if ok == false || len(schema) == 0 {
1430
- return schemaList
1431
- }
1432
- if oneOf, ok := schema["oneOf"].([]nativeiterate.JsonSchema); ok {
1433
- return oneOf
1423
+ output := make([]nativeiterate.JsonSchema, 0, len(schemaList))
1424
+ for _, schema := range schemaList {
1425
+ // The producer's reference identifies this use's component, which can
1426
+ // differ from the underlying array type when schema tags are present.
1427
+ ref, isReference := schema["$ref"].(string)
1428
+ key, isLocal := strings.CutPrefix(ref, "#/components/schemas/")
1429
+ target, exists := components.Schemas[key]
1430
+ if !isReference || !isLocal || !exists || len(target) == 0 {
1431
+ output = append(output, schema)
1432
+ continue
1433
+ }
1434
+ if oneOf, ok := target["oneOf"].([]nativeiterate.JsonSchema); ok {
1435
+ output = append(output, oneOf...)
1436
+ } else {
1437
+ output = append(output, target)
1438
+ }
1434
1439
  }
1435
- return []nativeiterate.JsonSchema{schema}
1440
+ return output
1436
1441
  }
1437
1442
 
1438
1443
  func randomProgrammer_internal(context nativecontext.ITypiaContext, name string) *shimast.Node {
@@ -1240,7 +1240,7 @@ func checkerProgrammer_decode_array(props checkerProgrammer_decodeArrayProps) *s
1240
1240
  if props.Array.Type.Index != nil {
1241
1241
  index = *props.Array.Type.Index
1242
1242
  }
1243
- return checkerProgrammer_or(
1243
+ main := checkerProgrammer_or(
1244
1244
  f.NewCallExpression(
1245
1245
  f.NewIdentifier(props.Functor.UseLocal(fmt.Sprintf("%sa%d", props.Config.Prefix, index))),
1246
1246
  nil,
@@ -1260,14 +1260,12 @@ func checkerProgrammer_decode_array(props checkerProgrammer_decodeArrayProps) *s
1260
1260
  }),
1261
1261
  props.Context.Emit,
1262
1262
  )
1263
+ // Structural traversal is shared and visit-guarded, but this use's tags must
1264
+ // be checked even when that helper has already visited the same array value.
1265
+ return checkerProgrammer_check_array(props, main)
1263
1266
  }
1264
1267
 
1265
1268
  func checkerProgrammer_decode_array_inline(props checkerProgrammer_decodeArrayInlineProps) *shimast.Node {
1266
- length := nativeiterate.Check_array_length(nativeiterate.Check_array_lengthProps{
1267
- Context: props.Context,
1268
- Array: props.Array,
1269
- Input: props.Input,
1270
- })
1271
1269
  main := FeatureProgrammer.Decode_array(FeatureProgrammer_DecodeArrayProps{
1272
1270
  Config: FeatureProgrammer_DecodeArrayConfig{
1273
1271
  Prefix: props.Config.Prefix,
@@ -1299,6 +1297,15 @@ func checkerProgrammer_decode_array_inline(props checkerProgrammer_decodeArrayIn
1299
1297
  Explore: props.Explore,
1300
1298
  Emit: props.Context.Emit,
1301
1299
  })
1300
+ return checkerProgrammer_check_array(props, main)
1301
+ }
1302
+
1303
+ func checkerProgrammer_check_array(props checkerProgrammer_decodeArrayProps, main *shimast.Node) *shimast.Node {
1304
+ length := nativeiterate.Check_array_length(nativeiterate.Check_array_lengthProps{
1305
+ Context: props.Context,
1306
+ Array: props.Array,
1307
+ Input: props.Input,
1308
+ })
1302
1309
  if length.Expression == nil && len(length.Conditions) == 0 {
1303
1310
  return main
1304
1311
  }
@@ -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,
@@ -0,0 +1,53 @@
1
+ package programmers
2
+
3
+ import (
4
+ "reflect"
5
+ "testing"
6
+
7
+ iterate "github.com/samchon/typia/packages/typia/native/core/programmers/iterate"
8
+ )
9
+
10
+ // TestRandomProgrammerArraySchemaReference verifies random generation consumes
11
+ // the schema writer's selected recursive variant rather than the bare type name.
12
+ //
13
+ // A recursive array can have several constrained schema components; only the
14
+ // returned reference identifies the constraints of the current use.
15
+ //
16
+ // 1. Supply a decoy plain component and a referenced maximum-zero variant.
17
+ // 2. Resolve a variant with multiple alternative tag rows.
18
+ // 3. Preserve inline, external and unresolved schemas without inventing a target.
19
+ //
20
+ // @evidence contracts/testing.md#behavioral-verification Resolves authored component references and compares complete schema lists, including explicit zero bounds and alternatives, as consumed by the random generator.
21
+ // @evidence contracts/testing.md#independent-expectations A local reference selects its named component, not a similarly named unconstrained component; literal array schema maps establish the expected constraints.
22
+ // @evidence contracts/testing.md#distinguishing-cases Distinguishes plain versus constrained targets, zero bounds, one versus multiple rows, inline schemas, nil components, absent targets and external references.
23
+ // @evidence contracts/testing.md#execution-ownership pnpm test:go:native discovers this same-package unit test; it executes the schema consumer in memory with no compiler or product process.
24
+ func TestRandomProgrammerArraySchemaReference(t *testing.T) {
25
+ empty := iterate.JsonSchema{"type": "array", "maxItems": 0}
26
+ multiple := []iterate.JsonSchema{empty, {"type": "array", "minItems": 2}}
27
+ components := &iterate.OpenApi_IComponents{Schemas: map[string]iterate.JsonSchema{
28
+ "Tree": {"type": "array"}, "Tree-array": empty,
29
+ "Tree-array-1": {"oneOf": multiple},
30
+ }}
31
+ for _, sample := range []struct {
32
+ name string
33
+ schema iterate.JsonSchema
34
+ expected []iterate.JsonSchema
35
+ }{
36
+ {"selected tagged target", iterate.JsonSchema{"$ref": "#/components/schemas/Tree-array"}, []iterate.JsonSchema{empty}},
37
+ {"alternative rows", iterate.JsonSchema{"$ref": "#/components/schemas/Tree-array-1"}, multiple},
38
+ {"inline", empty, []iterate.JsonSchema{empty}},
39
+ {"missing", iterate.JsonSchema{"$ref": "#/components/schemas/Missing"}, []iterate.JsonSchema{{"$ref": "#/components/schemas/Missing"}}},
40
+ {"external", iterate.JsonSchema{"$ref": "other.json#/Tree-array"}, []iterate.JsonSchema{{"$ref": "other.json#/Tree-array"}}},
41
+ } {
42
+ t.Run(sample.name, func(t *testing.T) {
43
+ actual := randomProgrammer_array_schema_list(components, []iterate.JsonSchema{sample.schema})
44
+ if !reflect.DeepEqual(actual, sample.expected) {
45
+ t.Errorf("got %#v, want %#v", actual, sample.expected)
46
+ }
47
+ })
48
+ }
49
+ original := []iterate.JsonSchema{empty}
50
+ if !reflect.DeepEqual(randomProgrammer_array_schema_list(nil, original), original) {
51
+ t.Error("nil components changed inline schema")
52
+ }
53
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typia",
3
- "version": "15.2.0",
3
+ "version": "15.2.1",
4
4
  "description": "Superfast runtime validators with only one line",
5
5
  "main": "lib/index.js",
6
6
  "exports": {
@@ -43,8 +43,8 @@
43
43
  "homepage": "https://typia.io",
44
44
  "dependencies": {
45
45
  "randexp": "^0.5.3",
46
- "@typia/interface": "^15.2.0",
47
- "@typia/utils": "^15.2.0"
46
+ "@typia/interface": "^15.2.1",
47
+ "@typia/utils": "^15.2.1"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "ttsc": ">=0.34.1"