anyvali 0.0.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.
Files changed (36) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +339 -0
  3. data/lib/anyvali/anyvali_document.rb +43 -0
  4. data/lib/anyvali/format/validators.rb +89 -0
  5. data/lib/anyvali/interchange/exporter.rb +40 -0
  6. data/lib/anyvali/interchange/importer.rb +201 -0
  7. data/lib/anyvali/issue_codes.rb +20 -0
  8. data/lib/anyvali/parse/coercion.rb +93 -0
  9. data/lib/anyvali/parse/coercion_config.rb +19 -0
  10. data/lib/anyvali/parse/defaults.rb +21 -0
  11. data/lib/anyvali/parse_result.rb +21 -0
  12. data/lib/anyvali/schema.rb +132 -0
  13. data/lib/anyvali/schemas/any_schema.rb +15 -0
  14. data/lib/anyvali/schemas/array_schema.rb +77 -0
  15. data/lib/anyvali/schemas/bool_schema.rb +22 -0
  16. data/lib/anyvali/schemas/enum_schema.rb +50 -0
  17. data/lib/anyvali/schemas/int_schema.rb +9 -0
  18. data/lib/anyvali/schemas/intersection_schema.rb +65 -0
  19. data/lib/anyvali/schemas/literal_schema.rb +52 -0
  20. data/lib/anyvali/schemas/never_schema.rb +20 -0
  21. data/lib/anyvali/schemas/null_schema.rb +22 -0
  22. data/lib/anyvali/schemas/nullable_schema.rb +40 -0
  23. data/lib/anyvali/schemas/number_schema.rb +214 -0
  24. data/lib/anyvali/schemas/object_schema.rb +150 -0
  25. data/lib/anyvali/schemas/optional_schema.rb +47 -0
  26. data/lib/anyvali/schemas/record_schema.rb +51 -0
  27. data/lib/anyvali/schemas/ref_schema.rb +58 -0
  28. data/lib/anyvali/schemas/string_schema.rb +113 -0
  29. data/lib/anyvali/schemas/tuple_schema.rb +72 -0
  30. data/lib/anyvali/schemas/union_schema.rb +50 -0
  31. data/lib/anyvali/schemas/unknown_schema.rb +15 -0
  32. data/lib/anyvali/validation_context.rb +11 -0
  33. data/lib/anyvali/validation_error.rb +13 -0
  34. data/lib/anyvali/validation_issue.rb +45 -0
  35. data/lib/anyvali.rb +176 -0
  36. metadata +107 -0
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyVali
4
+ class RefSchema < Schema
5
+ attr_reader :ref
6
+
7
+ def initialize(ref:, **kwargs)
8
+ @ref = ref
9
+ super(kind: "ref", **kwargs)
10
+ end
11
+
12
+ def to_node
13
+ { "kind" => "ref", "ref" => @ref }
14
+ end
15
+
16
+ def safe_parse(input, path: [], context: nil)
17
+ context ||= ValidationContext.new
18
+ resolved = resolve(context)
19
+ if resolved.nil?
20
+ issues = [ValidationIssue.new(
21
+ code: IssueCodes::UNSUPPORTED_SCHEMA_KIND,
22
+ path: path,
23
+ expected: @ref,
24
+ received: "unresolved ref"
25
+ )]
26
+ return ParseResult.new(value: nil, issues: issues)
27
+ end
28
+ resolved.safe_parse(input, path: path, context: context)
29
+ end
30
+
31
+ protected
32
+
33
+ def validate(value, path, issues, context)
34
+ # Handled in safe_parse override
35
+ end
36
+
37
+ def dup_with(**overrides)
38
+ attrs = {
39
+ ref: @ref,
40
+ kind: @kind,
41
+ constraints: @constraints,
42
+ coerce_config: @coerce_config,
43
+ default_value: @default_value,
44
+ has_default: @has_default,
45
+ custom_validators: @custom_validators
46
+ }.merge(overrides)
47
+ self.class.new(**attrs)
48
+ end
49
+
50
+ private
51
+
52
+ def resolve(context)
53
+ # ref format: "#/definitions/Name"
54
+ name = @ref.sub("#/definitions/", "")
55
+ context.definitions[name]
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyVali
4
+ class StringSchema < Schema
5
+ def initialize(constraints: {}, **kwargs)
6
+ super(kind: "string", constraints: constraints, **kwargs)
7
+ end
8
+
9
+ def min_length(n)
10
+ dup_with(constraints: @constraints.merge("minLength" => n))
11
+ end
12
+
13
+ def max_length(n)
14
+ dup_with(constraints: @constraints.merge("maxLength" => n))
15
+ end
16
+
17
+ def pattern(re)
18
+ pat = re.is_a?(Regexp) ? re.source : re
19
+ dup_with(constraints: @constraints.merge("pattern" => pat))
20
+ end
21
+
22
+ def starts_with(prefix)
23
+ dup_with(constraints: @constraints.merge("startsWith" => prefix))
24
+ end
25
+
26
+ def ends_with(suffix)
27
+ dup_with(constraints: @constraints.merge("endsWith" => suffix))
28
+ end
29
+
30
+ def includes(substr)
31
+ dup_with(constraints: @constraints.merge("includes" => substr))
32
+ end
33
+
34
+ def format(fmt)
35
+ dup_with(constraints: @constraints.merge("format" => fmt))
36
+ end
37
+
38
+ protected
39
+
40
+ def validate(value, path, issues, context)
41
+ unless value.is_a?(String)
42
+ issues << ValidationIssue.new(
43
+ code: IssueCodes::INVALID_TYPE,
44
+ path: path,
45
+ expected: "string",
46
+ received: Schema.type_name(value)
47
+ )
48
+ return
49
+ end
50
+
51
+ if @constraints["minLength"] && value.length < @constraints["minLength"]
52
+ issues << ValidationIssue.new(
53
+ code: IssueCodes::TOO_SMALL,
54
+ path: path,
55
+ expected: @constraints["minLength"].to_s,
56
+ received: value.length.to_s
57
+ )
58
+ end
59
+
60
+ if @constraints["maxLength"] && value.length > @constraints["maxLength"]
61
+ issues << ValidationIssue.new(
62
+ code: IssueCodes::TOO_LARGE,
63
+ path: path,
64
+ expected: @constraints["maxLength"].to_s,
65
+ received: value.length.to_s
66
+ )
67
+ end
68
+
69
+ if @constraints["pattern"]
70
+ re = Regexp.new(@constraints["pattern"])
71
+ unless re.match?(value)
72
+ issues << ValidationIssue.new(
73
+ code: IssueCodes::INVALID_STRING,
74
+ path: path,
75
+ expected: @constraints["pattern"],
76
+ received: value
77
+ )
78
+ end
79
+ end
80
+
81
+ if @constraints["startsWith"] && !value.start_with?(@constraints["startsWith"])
82
+ issues << ValidationIssue.new(
83
+ code: IssueCodes::INVALID_STRING,
84
+ path: path,
85
+ expected: @constraints["startsWith"],
86
+ received: value
87
+ )
88
+ end
89
+
90
+ if @constraints["endsWith"] && !value.end_with?(@constraints["endsWith"])
91
+ issues << ValidationIssue.new(
92
+ code: IssueCodes::INVALID_STRING,
93
+ path: path,
94
+ expected: @constraints["endsWith"],
95
+ received: value
96
+ )
97
+ end
98
+
99
+ if @constraints["includes"] && !value.include?(@constraints["includes"])
100
+ issues << ValidationIssue.new(
101
+ code: IssueCodes::INVALID_STRING,
102
+ path: path,
103
+ expected: @constraints["includes"],
104
+ received: value
105
+ )
106
+ end
107
+
108
+ if @constraints["format"]
109
+ Format::Validators.validate(value, @constraints["format"], path, issues)
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyVali
4
+ class TupleSchema < Schema
5
+ attr_reader :element_schemas
6
+
7
+ def initialize(elements:, **kwargs)
8
+ @element_schemas = elements.freeze
9
+ super(kind: "tuple", **kwargs)
10
+ end
11
+
12
+ def to_node
13
+ node = super
14
+ node["elements"] = @element_schemas.map(&:to_node)
15
+ node
16
+ end
17
+
18
+ protected
19
+
20
+ def validate(value, path, issues, context)
21
+ unless value.is_a?(Array)
22
+ issues << ValidationIssue.new(
23
+ code: IssueCodes::INVALID_TYPE,
24
+ path: path,
25
+ expected: "tuple",
26
+ received: Schema.type_name(value)
27
+ )
28
+ return
29
+ end
30
+
31
+ expected_len = @element_schemas.length
32
+ if value.length < expected_len
33
+ issues << ValidationIssue.new(
34
+ code: IssueCodes::TOO_SMALL,
35
+ path: path,
36
+ expected: expected_len.to_s,
37
+ received: value.length.to_s
38
+ )
39
+ return
40
+ end
41
+
42
+ if value.length > expected_len
43
+ issues << ValidationIssue.new(
44
+ code: IssueCodes::TOO_LARGE,
45
+ path: path,
46
+ expected: expected_len.to_s,
47
+ received: value.length.to_s
48
+ )
49
+ return
50
+ end
51
+
52
+ @element_schemas.each_with_index do |schema, i|
53
+ result = schema.safe_parse(value[i], path: path + [i], context: context)
54
+ issues.concat(result.issues) if result.failure?
55
+ end
56
+ end
57
+
58
+ def dup_with(**overrides)
59
+ elements = overrides.delete(:elements) || @element_schemas
60
+ attrs = {
61
+ elements: elements,
62
+ kind: @kind,
63
+ constraints: @constraints,
64
+ coerce_config: @coerce_config,
65
+ default_value: @default_value,
66
+ has_default: @has_default,
67
+ custom_validators: @custom_validators
68
+ }.merge(overrides)
69
+ self.class.new(**attrs)
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyVali
4
+ class UnionSchema < Schema
5
+ attr_reader :variant_schemas
6
+
7
+ def initialize(variants:, **kwargs)
8
+ @variant_schemas = variants.freeze
9
+ super(kind: "union", **kwargs)
10
+ end
11
+
12
+ def to_node
13
+ node = super
14
+ node["variants"] = @variant_schemas.map(&:to_node)
15
+ node
16
+ end
17
+
18
+ protected
19
+
20
+ def validate(value, path, issues, context)
21
+ @variant_schemas.each do |schema|
22
+ result = schema.safe_parse(value, path: path, context: context)
23
+ return if result.success?
24
+ end
25
+
26
+ # No variant matched
27
+ expected = @variant_schemas.map(&:kind).join(" | ")
28
+ issues << ValidationIssue.new(
29
+ code: IssueCodes::INVALID_UNION,
30
+ path: path,
31
+ expected: expected,
32
+ received: Schema.type_name(value)
33
+ )
34
+ end
35
+
36
+ def dup_with(**overrides)
37
+ variants = overrides.delete(:variants) || @variant_schemas
38
+ attrs = {
39
+ variants: variants,
40
+ kind: @kind,
41
+ constraints: @constraints,
42
+ coerce_config: @coerce_config,
43
+ default_value: @default_value,
44
+ has_default: @has_default,
45
+ custom_validators: @custom_validators
46
+ }.merge(overrides)
47
+ self.class.new(**attrs)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyVali
4
+ class UnknownSchema < Schema
5
+ def initialize(**kwargs)
6
+ super(kind: "unknown", **kwargs)
7
+ end
8
+
9
+ protected
10
+
11
+ def validate(value, path, issues, context)
12
+ # unknown accepts everything
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyVali
4
+ class ValidationContext
5
+ attr_reader :definitions
6
+
7
+ def initialize(definitions: {})
8
+ @definitions = definitions
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyVali
4
+ class ValidationError < StandardError
5
+ attr_reader :issues
6
+
7
+ def initialize(issues)
8
+ @issues = issues
9
+ messages = issues.map { |i| "#{i.code} at #{i.path.inspect}: expected #{i.expected}, received #{i.received}" }
10
+ super("Validation failed: #{messages.join('; ')}")
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyVali
4
+ class ValidationIssue
5
+ attr_reader :code, :message, :path, :expected, :received, :meta
6
+
7
+ def initialize(code:, message: nil, path: [], expected: nil, received: nil, meta: nil)
8
+ @code = code
9
+ @message = message || default_message(code)
10
+ @path = path.freeze
11
+ @expected = expected
12
+ @received = received
13
+ @meta = meta
14
+ freeze
15
+ end
16
+
17
+ def to_h
18
+ h = { "code" => @code, "path" => @path.dup }
19
+ h["expected"] = @expected unless @expected.nil?
20
+ h["received"] = @received unless @received.nil?
21
+ h["message"] = @message if @message
22
+ h["meta"] = @meta if @meta
23
+ h
24
+ end
25
+
26
+ private
27
+
28
+ def default_message(code)
29
+ case code
30
+ when IssueCodes::INVALID_TYPE then "Invalid type"
31
+ when IssueCodes::REQUIRED then "Required field missing"
32
+ when IssueCodes::UNKNOWN_KEY then "Unknown key"
33
+ when IssueCodes::TOO_SMALL then "Value too small"
34
+ when IssueCodes::TOO_LARGE then "Value too large"
35
+ when IssueCodes::INVALID_STRING then "Invalid string"
36
+ when IssueCodes::INVALID_NUMBER then "Invalid number"
37
+ when IssueCodes::INVALID_LITERAL then "Invalid literal"
38
+ when IssueCodes::INVALID_UNION then "Invalid union"
39
+ when IssueCodes::COERCION_FAILED then "Coercion failed"
40
+ when IssueCodes::DEFAULT_INVALID then "Default value is invalid"
41
+ else "Validation failed"
42
+ end
43
+ end
44
+ end
45
+ end
data/lib/anyvali.rb ADDED
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "anyvali/issue_codes"
4
+ require_relative "anyvali/validation_issue"
5
+ require_relative "anyvali/parse_result"
6
+ require_relative "anyvali/validation_error"
7
+ require_relative "anyvali/validation_context"
8
+ require_relative "anyvali/anyvali_document"
9
+ require_relative "anyvali/schema"
10
+
11
+ require_relative "anyvali/parse/coercion"
12
+ require_relative "anyvali/parse/coercion_config"
13
+ require_relative "anyvali/parse/defaults"
14
+
15
+ require_relative "anyvali/format/validators"
16
+
17
+ require_relative "anyvali/schemas/string_schema"
18
+ require_relative "anyvali/schemas/number_schema"
19
+ require_relative "anyvali/schemas/int_schema"
20
+ require_relative "anyvali/schemas/bool_schema"
21
+ require_relative "anyvali/schemas/null_schema"
22
+ require_relative "anyvali/schemas/any_schema"
23
+ require_relative "anyvali/schemas/unknown_schema"
24
+ require_relative "anyvali/schemas/never_schema"
25
+ require_relative "anyvali/schemas/literal_schema"
26
+ require_relative "anyvali/schemas/enum_schema"
27
+ require_relative "anyvali/schemas/array_schema"
28
+ require_relative "anyvali/schemas/tuple_schema"
29
+ require_relative "anyvali/schemas/object_schema"
30
+ require_relative "anyvali/schemas/record_schema"
31
+ require_relative "anyvali/schemas/union_schema"
32
+ require_relative "anyvali/schemas/intersection_schema"
33
+ require_relative "anyvali/schemas/optional_schema"
34
+ require_relative "anyvali/schemas/nullable_schema"
35
+ require_relative "anyvali/schemas/ref_schema"
36
+
37
+ require_relative "anyvali/interchange/exporter"
38
+ require_relative "anyvali/interchange/importer"
39
+
40
+ module AnyVali
41
+ VERSION = "0.0.1"
42
+
43
+ module_function
44
+
45
+ # Builder methods
46
+
47
+ def string
48
+ StringSchema.new
49
+ end
50
+
51
+ def number
52
+ NumberSchema.new(kind: "number")
53
+ end
54
+
55
+ def float32
56
+ NumberSchema.new(kind: "float32")
57
+ end
58
+
59
+ def float64
60
+ NumberSchema.new(kind: "float64")
61
+ end
62
+
63
+ def int_
64
+ IntSchema.new(kind: "int")
65
+ end
66
+
67
+ def int8
68
+ IntSchema.new(kind: "int8")
69
+ end
70
+
71
+ def int16
72
+ IntSchema.new(kind: "int16")
73
+ end
74
+
75
+ def int32
76
+ IntSchema.new(kind: "int32")
77
+ end
78
+
79
+ def int64
80
+ IntSchema.new(kind: "int64")
81
+ end
82
+
83
+ def uint8
84
+ IntSchema.new(kind: "uint8")
85
+ end
86
+
87
+ def uint16
88
+ IntSchema.new(kind: "uint16")
89
+ end
90
+
91
+ def uint32
92
+ IntSchema.new(kind: "uint32")
93
+ end
94
+
95
+ def uint64
96
+ IntSchema.new(kind: "uint64")
97
+ end
98
+
99
+ def bool
100
+ BoolSchema.new
101
+ end
102
+
103
+ def null
104
+ NullSchema.new
105
+ end
106
+
107
+ def any
108
+ AnySchema.new
109
+ end
110
+
111
+ def unknown
112
+ UnknownSchema.new
113
+ end
114
+
115
+ def never
116
+ NeverSchema.new
117
+ end
118
+
119
+ def literal(value)
120
+ LiteralSchema.new(value: value)
121
+ end
122
+
123
+ def enum_(*values)
124
+ EnumSchema.new(values: values.flatten)
125
+ end
126
+
127
+ def array(items)
128
+ ArraySchema.new(items: items)
129
+ end
130
+
131
+ def tuple(*elements)
132
+ TupleSchema.new(elements: elements.flatten)
133
+ end
134
+
135
+ def object(properties:, required: [], unknown_keys: "reject")
136
+ ObjectSchema.new(properties: properties, required: required, unknown_keys: unknown_keys)
137
+ end
138
+
139
+ def record(values)
140
+ RecordSchema.new(values: values)
141
+ end
142
+
143
+ def union(*variants)
144
+ UnionSchema.new(variants: variants.flatten)
145
+ end
146
+
147
+ def intersection(*schemas)
148
+ IntersectionSchema.new(all_of: schemas.flatten)
149
+ end
150
+
151
+ def optional(schema)
152
+ OptionalSchema.new(schema: schema)
153
+ end
154
+
155
+ def nullable(schema)
156
+ NullableSchema.new(schema: schema)
157
+ end
158
+
159
+ def ref(ref_path)
160
+ RefSchema.new(ref: ref_path)
161
+ end
162
+
163
+ # Interchange
164
+
165
+ def export(schema, mode: :portable, definitions: {})
166
+ Interchange::Exporter.export(schema, mode: mode, definitions: definitions)
167
+ end
168
+
169
+ def import(doc)
170
+ Interchange::Importer.import(doc)
171
+ end
172
+
173
+ def import_schema(doc)
174
+ Interchange::Importer.import_schema(doc)
175
+ end
176
+ end
metadata ADDED
@@ -0,0 +1,107 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: anyvali
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - AnyVali Contributors
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-03-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: minitest
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '13.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '13.0'
41
+ description: AnyVali Ruby SDK - native validation with portable schema interchange
42
+ across 10 languages
43
+ email:
44
+ - hello@anyvali.com
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - README.md
50
+ - lib/anyvali.rb
51
+ - lib/anyvali/anyvali_document.rb
52
+ - lib/anyvali/format/validators.rb
53
+ - lib/anyvali/interchange/exporter.rb
54
+ - lib/anyvali/interchange/importer.rb
55
+ - lib/anyvali/issue_codes.rb
56
+ - lib/anyvali/parse/coercion.rb
57
+ - lib/anyvali/parse/coercion_config.rb
58
+ - lib/anyvali/parse/defaults.rb
59
+ - lib/anyvali/parse_result.rb
60
+ - lib/anyvali/schema.rb
61
+ - lib/anyvali/schemas/any_schema.rb
62
+ - lib/anyvali/schemas/array_schema.rb
63
+ - lib/anyvali/schemas/bool_schema.rb
64
+ - lib/anyvali/schemas/enum_schema.rb
65
+ - lib/anyvali/schemas/int_schema.rb
66
+ - lib/anyvali/schemas/intersection_schema.rb
67
+ - lib/anyvali/schemas/literal_schema.rb
68
+ - lib/anyvali/schemas/never_schema.rb
69
+ - lib/anyvali/schemas/null_schema.rb
70
+ - lib/anyvali/schemas/nullable_schema.rb
71
+ - lib/anyvali/schemas/number_schema.rb
72
+ - lib/anyvali/schemas/object_schema.rb
73
+ - lib/anyvali/schemas/optional_schema.rb
74
+ - lib/anyvali/schemas/record_schema.rb
75
+ - lib/anyvali/schemas/ref_schema.rb
76
+ - lib/anyvali/schemas/string_schema.rb
77
+ - lib/anyvali/schemas/tuple_schema.rb
78
+ - lib/anyvali/schemas/union_schema.rb
79
+ - lib/anyvali/schemas/unknown_schema.rb
80
+ - lib/anyvali/validation_context.rb
81
+ - lib/anyvali/validation_error.rb
82
+ - lib/anyvali/validation_issue.rb
83
+ homepage: https://anyvali.com
84
+ licenses:
85
+ - MIT
86
+ metadata:
87
+ source_code_uri: https://github.com/BetterCorp/AnyVali
88
+ post_install_message:
89
+ rdoc_options: []
90
+ require_paths:
91
+ - lib
92
+ required_ruby_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: 3.0.0
97
+ required_rubygems_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ requirements: []
103
+ rubygems_version: 3.5.22
104
+ signing_key:
105
+ specification_version: 4
106
+ summary: Native validation with portable schema interchange
107
+ test_files: []