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.
- package/lib/internal/_stringLengthGte.d.ts +7 -3
- package/lib/internal/_stringLengthGte.js +9 -3
- package/lib/internal/_stringLengthGte.js.map +1 -1
- package/lib/internal/_stringLengthGte.mjs +8 -3
- package/lib/internal/_stringLengthGte.mjs.map +1 -1
- package/lib/internal/_stringLengthLte.d.ts +7 -3
- package/lib/internal/_stringLengthLte.js +9 -3
- package/lib/internal/_stringLengthLte.js.map +1 -1
- package/lib/internal/_stringLengthLte.mjs +8 -3
- package/lib/internal/_stringLengthLte.mjs.map +1 -1
- package/native/cmd/ttsc-typia/json_schema_recursive_array_alias_transform_test.go +79 -0
- package/native/core/context/ImportProgrammer.go +19 -9
- package/native/core/context/ImportProgrammerPattern.go +36 -0
- package/native/core/factories/internal/metadata/iterate_metadata_intersection.go +4 -1
- package/native/core/programmers/RandomProgrammer.go +19 -14
- package/native/core/programmers/internal/CheckerProgrammer.go +13 -6
- package/native/core/programmers/iterate/check_string.go +41 -5
- package/native/core/programmers/iterate/check_string_pattern_test.go +91 -0
- package/native/core/programmers/iterate/check_template.go +4 -4
- package/native/core/programmers/iterate/json_schema_alias.go +22 -24
- package/native/core/programmers/iterate/json_schema_array.go +14 -10
- package/native/core/programmers/iterate/json_schema_array_names.go +113 -0
- package/native/core/programmers/iterate/json_schema_native.go +10 -3
- package/native/core/programmers/iterate/json_schema_recursive_array_identity_test.go +75 -0
- package/native/core/programmers/iterate/json_schema_recursive_array_ownership_test.go +112 -0
- package/native/core/programmers/iterate/json_schema_station.go +5 -1
- package/native/core/programmers/json/JsonSchemasProgrammer.go +1 -0
- package/native/core/programmers/random_programmer_array_schema_reference_test.go +53 -0
- package/package.json +3 -3
- package/src/internal/_stringLengthGte.ts +8 -3
- 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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
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/" +
|
|
42
|
+
return []JsonSchema{{"$ref": "#/components/schemas/" + key}}
|
|
41
43
|
}
|
|
42
44
|
if props.components.Schemas != nil {
|
|
43
|
-
if _, ok := props.components.Schemas[
|
|
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[
|
|
51
|
-
props.components.emplaceSchemaKey(
|
|
52
|
-
props.components.Schemas[
|
|
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[
|
|
58
|
+
json_schema_assign(props.components.Schemas[key], oneOf[0])
|
|
57
59
|
} else {
|
|
58
|
-
props.components.Schemas[
|
|
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
|
|
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
|
|
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
|
|
12
|
-
// @evidence contracts/common.md#clear-and-simple-design
|
|
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
|
-
|
|
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,
|