anyvali 0.0.1 → 0.1.6

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7c53ac42dbac09b5549467769f54d5597699feb351c0f77975cced4cb11bd569
4
- data.tar.gz: c73e7af5ae8c388e30fe3e9040a93bbd212d0d1ad16a05761d8db2bb4fe0348f
3
+ metadata.gz: c865ca84583452f4064bf2789277cd979c33e32f718aa3ed3653e58de25e0158
4
+ data.tar.gz: 25de1308d962585be84d11b97830acbb5d399d2e401411e98fdd54ea7375a28d
5
5
  SHA512:
6
- metadata.gz: e3f43ec3aa8ff4310fd866208fadb24997c0304fb0cfda21ed8967d614b5695abd345ed63fc47dbedb39acfa5b97b183f61357c042ef723007d96ab43fb8a2e4
7
- data.tar.gz: dbf001ffb2e24ac3d881fe4066b2f675f44f66f47123b684ab8828b8adf441f76c7d5b8665e91fce3939493e06b62e58f2a99902fda0a33869904b3daf640b59
6
+ metadata.gz: b89bc74437da5378dc0d7417a3a50165796f179d3aeb501ed3b73db55e39fb13146fcc3a7f6d7efb0041cffda6818009c5ba0b552768a930d9b9ed5ea7be0ccf
7
+ data.tar.gz: ada9ceee7bcb4c9782378168814b14f1e7767e387d54247a24877ef73e107cb55072b411097d7841afd0e9cda5154cd11eaedc92c3730e4b68d45e296598fcb1
data/README.md CHANGED
@@ -10,8 +10,9 @@
10
10
 
11
11
  <p align="center">
12
12
  <a href="https://github.com/BetterCorp/AnyVali/actions/workflows/ci.yml"><img src="https://github.com/BetterCorp/AnyVali/actions/workflows/ci.yml/badge.svg" alt="CI" /></a>
13
+ <a href="https://codecov.io/gh/BetterCorp/AnyVali"><img src="https://codecov.io/gh/BetterCorp/AnyVali/graph/badge.svg?token=3H068H51QN" alt="codecov" /></a>
13
14
  <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License: MIT" /></a>
14
- <a href="https://www.npmjs.com/package/@anyvali/js"><img src="https://img.shields.io/npm/v/%40anyvali%2Fjs.svg?label=npm" alt="npm" /></a>
15
+ <a href="https://www.npmjs.com/package/anyvali"><img src="https://img.shields.io/npm/v/anyvali.svg?label=npm" alt="npm" /></a>
15
16
  <a href="https://pypi.org/project/anyvali/"><img src="https://img.shields.io/pypi/v/anyvali.svg?label=pypi" alt="PyPI" /></a>
16
17
  <a href="https://crates.io/crates/anyvali"><img src="https://img.shields.io/crates/v/anyvali.svg?label=crates.io" alt="crates.io" /></a>
17
18
  <a href="https://pkg.go.dev/github.com/BetterCorp/AnyVali/sdk/go"><img src="https://img.shields.io/badge/go-pkg.go.dev-blue.svg" alt="Go" /></a>
@@ -41,7 +42,7 @@ AnyVali lets you write validation schemas in your language, then share them acro
41
42
  ## Install
42
43
 
43
44
  ```bash
44
- npm install @anyvali/js # JavaScript / TypeScript
45
+ npm install anyvali # JavaScript / TypeScript
45
46
  pip install anyvali # Python
46
47
  go get github.com/BetterCorp/AnyVali/sdk/go # Go
47
48
  cargo add anyvali # Rust
@@ -85,7 +86,7 @@ Define a schema, parse input, get structured errors or clean data.
85
86
  <td>
86
87
 
87
88
  ```typescript
88
- import { string, int, object, array } from "@anyvali/js";
89
+ import { string, int, object, array } from "anyvali";
89
90
 
90
91
  const User = object({
91
92
  name: string().minLength(1),
@@ -152,6 +153,36 @@ if !result.Success {
152
153
  ```
153
154
  </details>
154
155
 
156
+ ## Type Inference
157
+
158
+ All 10 SDKs now provide static type inference, so parsed values carry the correct type without manual casts. The TypeScript SDK offers full Zod-style `Infer<T>`:
159
+
160
+ ```typescript
161
+ import { object, string, int, type Infer } from "anyvali";
162
+
163
+ const User = object({
164
+ name: string().minLength(1),
165
+ email: string().format('email'),
166
+ age: int().min(0).optional(),
167
+ });
168
+
169
+ type User = Infer<typeof User>;
170
+ // => { name: string; email: string; age?: number | undefined }
171
+
172
+ const user = User.parse(input); // fully typed, no cast needed
173
+ ```
174
+
175
+ Other SDKs use the type inference mechanism native to each language:
176
+
177
+ - **Python** -- `BaseSchema(Generic[T])`, `ParseResult(Generic[T])`; `parse()` returns `T`
178
+ - **C# / Kotlin** -- `Schema<T>` generic base class, `ParseResult<T>`
179
+ - **Java** -- `Schema<T>` generic base, `ParseResult<T>` record
180
+ - **Go** -- `TypedParse[T]()` and `TypedSafeParse[T]()` generic helper functions
181
+ - **Rust** -- `TypedSchema` trait with associated `Output` type, `parse_as<T>()` free function
182
+ - **C++** -- Template `parse_as<T>()` and `safe_parse_as<T>()` helpers
183
+ - **PHP** -- `@template` phpDoc annotations for PHPStan/Psalm
184
+ - **Ruby** -- RBS type signature file for Steep/Sorbet
185
+
155
186
  ## Cross-Language Schema Sharing
156
187
 
157
188
  AnyVali's core feature: export a schema from one language, import it in another.
@@ -176,8 +207,8 @@ result = schema.safe_parse(request_body) # Same validation rules!
176
207
  The JS SDK also ships a small forms layer for browser-native fields, HTML5 attributes, and AnyVali validation.
177
208
 
178
209
  ```typescript
179
- import { object, string, int } from "@anyvali/js";
180
- import { initForm } from "@anyvali/js/forms";
210
+ import { object, string, int } from "anyvali";
211
+ import { initForm } from "anyvali/forms";
181
212
 
182
213
  const Signup = object({
183
214
  email: string().format("email"),
@@ -198,8 +229,8 @@ initForm("#signup", { schema: Signup });
198
229
  For JSX-style attribute binding:
199
230
 
200
231
  ```tsx
201
- import { object, string } from "@anyvali/js";
202
- import { createFormBindings } from "@anyvali/js/forms";
232
+ import { object, string } from "anyvali";
233
+ import { createFormBindings } from "anyvali/forms";
203
234
 
204
235
  const Signup = object({
205
236
  email: string().format("email"),
@@ -223,7 +254,7 @@ The portable JSON format:
223
254
  "email": { "kind": "string", "format": "email" }
224
255
  },
225
256
  "required": ["name", "email"],
226
- "unknownKeys": "reject"
257
+ "unknownKeys": "strip"
227
258
  },
228
259
  "definitions": {},
229
260
  "extensions": {}
@@ -234,7 +265,7 @@ The portable JSON format:
234
265
 
235
266
  | Language | Package | Status |
236
267
  |----------|---------|--------|
237
- | JavaScript / TypeScript | [`@anyvali/js`](https://www.npmjs.com/package/@anyvali/js) | v0.0.1 |
268
+ | JavaScript / TypeScript | [`anyvali`](https://www.npmjs.com/package/anyvali) | v0.0.1 |
238
269
  | Python | [`anyvali`](https://pypi.org/project/anyvali/) | v0.0.1 |
239
270
  | Go | [`github.com/BetterCorp/AnyVali/sdk/go`](https://pkg.go.dev/github.com/BetterCorp/AnyVali/sdk/go) | v0.0.1 |
240
271
  | Java | `com.anyvali:anyvali` | v0.0.1 |
@@ -144,7 +144,9 @@ module AnyVali
144
144
  end
145
145
 
146
146
  def build_array(node)
147
- items = node_to_schema(node["items"])
147
+ # Accept "items", "item", and "array.items" keys for compatibility
148
+ items_node = node["items"] || node["item"] || node["array.items"]
149
+ items = node_to_schema(items_node)
148
150
  constraints = {}
149
151
  %w[minItems maxItems].each do |k|
150
152
  constraints[k] = node[k] if node.key?(k)
@@ -163,11 +165,12 @@ module AnyVali
163
165
  props[k] = node_to_schema(v)
164
166
  end
165
167
  required = node["required"] || []
166
- unknown_keys = node["unknownKeys"] || "reject"
168
+ unknown_keys = node["unknownKeys"] || "strip"
167
169
  ObjectSchema.new(
168
170
  properties: props,
169
171
  required: required,
170
- unknown_keys: unknown_keys
172
+ unknown_keys: unknown_keys,
173
+ unknown_keys_explicit: true
171
174
  )
172
175
  end
173
176
 
@@ -177,7 +180,9 @@ module AnyVali
177
180
  end
178
181
 
179
182
  def build_union(node)
180
- variants = node["variants"].map { |v| node_to_schema(v) }
183
+ # Accept "variants", "schemas", and "union.variants" keys for compatibility
184
+ variants_data = node["variants"] || node["schemas"] || node["union.variants"] || []
185
+ variants = variants_data.map { |v| node_to_schema(v) }
181
186
  UnionSchema.new(variants: variants)
182
187
  end
183
188
 
@@ -53,6 +53,11 @@ module AnyVali
53
53
  return { success: false, value: value } unless value.is_a?(String)
54
54
 
55
55
  stripped = value.strip
56
+ # Spec 5.1: parse DECIMAL floating-point only. Ruby's Float() also accepts
57
+ # digit-group underscores ("1_000.5") and hex floats ("0x1.8p3"), which
58
+ # diverge from the JS reference and let non-decimal strings slip through.
59
+ return { success: false, value: value } unless stripped.match?(/\A[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?\z/)
60
+
56
61
  begin
57
62
  f = Float(stripped)
58
63
  { success: true, value: f }
@@ -3,15 +3,18 @@
3
3
  module AnyVali
4
4
  class Schema
5
5
  attr_reader :kind, :constraints, :coerce_config, :default_value, :has_default,
6
- :custom_validators
6
+ :custom_validators, :metadata
7
7
 
8
- def initialize(kind:, constraints: {}, coerce_config: nil, default_value: nil, has_default: false, custom_validators: [])
8
+ RESERVED_METADATA_KEYS = %w[title description deprecated deprecatedMessage notStable since sensitive readonly writeonly examples].freeze
9
+
10
+ def initialize(kind:, constraints: {}, coerce_config: nil, default_value: nil, has_default: false, custom_validators: [], metadata: {})
9
11
  @kind = kind
10
12
  @constraints = constraints.freeze
11
13
  @coerce_config = coerce_config
12
14
  @default_value = default_value
13
15
  @has_default = has_default
14
16
  @custom_validators = custom_validators.freeze
17
+ @metadata = metadata.freeze
15
18
  end
16
19
 
17
20
  def parse(input)
@@ -67,6 +70,70 @@ module AnyVali
67
70
  dup_with(coerce_config: config)
68
71
  end
69
72
 
73
+ def describe(description, **opts)
74
+ raise ArgumentError, "describe(): description must be a string" unless description.is_a?(String)
75
+
76
+ meta = { "description" => description }
77
+
78
+ if opts.key?(:title)
79
+ raise ArgumentError, "describe(): title must be a string" unless opts[:title].is_a?(String)
80
+ meta["title"] = opts[:title]
81
+ end
82
+ if opts.key?(:deprecated)
83
+ raise ArgumentError, "describe(): deprecated must be a boolean" unless [true, false].include?(opts[:deprecated])
84
+ meta["deprecated"] = opts[:deprecated]
85
+ end
86
+ if opts.key?(:deprecated_message)
87
+ raise ArgumentError, "describe(): deprecatedMessage must be a string" unless opts[:deprecated_message].is_a?(String)
88
+ raise ArgumentError, "describe(): deprecatedMessage requires deprecated: true" unless opts[:deprecated]
89
+ meta["deprecatedMessage"] = opts[:deprecated_message]
90
+ end
91
+ if opts.key?(:not_stable)
92
+ raise ArgumentError, "describe(): notStable must be a boolean" unless [true, false].include?(opts[:not_stable])
93
+ meta["notStable"] = opts[:not_stable]
94
+ end
95
+ if opts.key?(:since)
96
+ raise ArgumentError, "describe(): since must be a string" unless opts[:since].is_a?(String)
97
+ meta["since"] = opts[:since]
98
+ end
99
+ if opts.key?(:sensitive)
100
+ raise ArgumentError, "describe(): sensitive must be a boolean" unless [true, false].include?(opts[:sensitive])
101
+ meta["sensitive"] = opts[:sensitive]
102
+ end
103
+ if opts.key?(:readonly)
104
+ raise ArgumentError, "describe(): readonly must be a boolean" unless [true, false].include?(opts[:readonly])
105
+ meta["readonly"] = opts[:readonly]
106
+ end
107
+ if opts.key?(:writeonly)
108
+ raise ArgumentError, "describe(): writeonly must be a boolean" unless [true, false].include?(opts[:writeonly])
109
+ meta["writeonly"] = opts[:writeonly]
110
+ end
111
+ if opts[:readonly] && opts[:writeonly]
112
+ raise ArgumentError, "describe(): readonly and writeonly cannot both be true"
113
+ end
114
+ if opts.key?(:examples)
115
+ raise ArgumentError, "describe(): examples must be an array" unless opts[:examples].is_a?(Array)
116
+ meta["examples"] = opts[:examples]
117
+ end
118
+
119
+ dup_with(metadata: @metadata.merge(meta))
120
+ end
121
+
122
+ def with_metadata(meta, replace: false)
123
+ meta.each_key do |key|
124
+ if RESERVED_METADATA_KEYS.include?(key)
125
+ raise ArgumentError, "with_metadata(): \"#{key}\" is a reserved key. Use describe() instead."
126
+ end
127
+ end
128
+
129
+ if replace
130
+ preserved = @metadata.select { |k, _| RESERVED_METADATA_KEYS.include?(k) }
131
+ dup_with(metadata: preserved.merge(meta))
132
+ else
133
+ dup_with(metadata: @metadata.merge(meta))
134
+ end
135
+ end
136
+
70
137
  def refine(&block)
71
138
  dup_with(custom_validators: @custom_validators + [block])
72
139
  end
@@ -94,6 +161,7 @@ module AnyVali
94
161
  @constraints.each { |k, v| node[k] = v }
95
162
  node["coerce"] = @coerce_config if @coerce_config
96
163
  node["default"] = @default_value if @has_default
164
+ node["metadata"] = @metadata.dup unless @metadata.empty?
97
165
  node
98
166
  end
99
167
 
@@ -110,7 +178,8 @@ module AnyVali
110
178
  coerce_config: @coerce_config,
111
179
  default_value: @default_value,
112
180
  has_default: @has_default,
113
- custom_validators: @custom_validators
181
+ custom_validators: @custom_validators,
182
+ metadata: @metadata
114
183
  }.merge(overrides)
115
184
  self.class.new(**attrs)
116
185
  end
@@ -70,6 +70,16 @@ module AnyVali
70
70
  return
71
71
  end
72
72
 
73
+ if value.is_a?(Float) && (value.nan? || value.infinite?)
74
+ issues << ValidationIssue.new(
75
+ code: IssueCodes::INVALID_NUMBER,
76
+ path: path,
77
+ expected: @kind,
78
+ received: value.to_s
79
+ )
80
+ return
81
+ end
82
+
73
83
  # Must be a whole number
74
84
  if value.is_a?(Float) && value != value.floor
75
85
  issues << ValidationIssue.new(
@@ -120,6 +130,16 @@ module AnyVali
120
130
  return
121
131
  end
122
132
 
133
+ if value.is_a?(Float) && (value.nan? || value.infinite?)
134
+ issues << ValidationIssue.new(
135
+ code: IssueCodes::INVALID_NUMBER,
136
+ path: path,
137
+ expected: @kind,
138
+ received: value.to_s
139
+ )
140
+ return
141
+ end
142
+
123
143
  # For booleans (true/false are not Numeric in Ruby, so this is fine)
124
144
  if value == true || value == false
125
145
  issues << ValidationIssue.new(
@@ -4,23 +4,24 @@ module AnyVali
4
4
  class ObjectSchema < Schema
5
5
  attr_reader :properties, :required_keys, :unknown_keys
6
6
 
7
- def initialize(properties:, required: [], unknown_keys: "reject", **kwargs)
7
+ def initialize(properties:, required: [], unknown_keys: "strip", unknown_keys_explicit: false, **kwargs)
8
8
  @properties = properties.freeze
9
9
  @required_keys = required.freeze
10
10
  @unknown_keys = unknown_keys
11
+ @unknown_keys_explicit = unknown_keys_explicit
11
12
  super(kind: "object", **kwargs)
12
13
  end
13
14
 
14
15
  def strict
15
- dup_with(unknown_keys: "reject")
16
+ dup_with(unknown_keys: "reject", unknown_keys_explicit: true)
16
17
  end
17
18
 
18
19
  def strip_unknown
19
- dup_with(unknown_keys: "strip")
20
+ dup_with(unknown_keys: "strip", unknown_keys_explicit: true)
20
21
  end
21
22
 
22
23
  def allow_unknown
23
- dup_with(unknown_keys: "allow")
24
+ dup_with(unknown_keys: "allow", unknown_keys_explicit: true)
24
25
  end
25
26
 
26
27
  def to_node
@@ -31,7 +32,7 @@ module AnyVali
31
32
  end
32
33
  node["properties"] = props
33
34
  node["required"] = @required_keys
34
- node["unknownKeys"] = @unknown_keys
35
+ node["unknownKeys"] = export_unknown_keys
35
36
  node
36
37
  end
37
38
 
@@ -103,7 +104,7 @@ module AnyVali
103
104
 
104
105
  # Handle unknown keys
105
106
  unknown = input.keys - @properties.keys
106
- case @unknown_keys
107
+ case effective_unknown_keys
107
108
  when "reject"
108
109
  unknown.each do |key|
109
110
  issues << ValidationIssue.new(
@@ -137,6 +138,7 @@ module AnyVali
137
138
  properties: @properties,
138
139
  required: @required_keys,
139
140
  unknown_keys: @unknown_keys,
141
+ unknown_keys_explicit: @unknown_keys_explicit,
140
142
  kind: @kind,
141
143
  constraints: @constraints,
142
144
  coerce_config: @coerce_config,
@@ -146,5 +148,15 @@ module AnyVali
146
148
  }.merge(overrides)
147
149
  self.class.new(**attrs)
148
150
  end
151
+
152
+ private
153
+
154
+ def effective_unknown_keys
155
+ @unknown_keys_explicit ? @unknown_keys : "strip"
156
+ end
157
+
158
+ def export_unknown_keys
159
+ @unknown_keys_explicit ? @unknown_keys : "strip"
160
+ end
149
161
  end
150
162
  end
@@ -25,7 +25,25 @@ module AnyVali
25
25
  )]
26
26
  return ParseResult.new(value: nil, issues: issues)
27
27
  end
28
- resolved.safe_parse(input, path: path, context: context)
28
+ context.instance_variable_set(:@active_refs, {}) unless context.instance_variable_defined?(:@active_refs)
29
+ active_refs = context.instance_variable_get(:@active_refs)
30
+ key = "#{@ref}:#{input.object_id}"
31
+ if active_refs[key]
32
+ return ParseResult.new(value: nil, issues: [
33
+ ValidationIssue.new(
34
+ code: IssueCodes::INVALID_TYPE,
35
+ path: path,
36
+ expected: @ref,
37
+ received: "recursive ref"
38
+ )
39
+ ])
40
+ end
41
+ active_refs[key] = true
42
+ begin
43
+ resolved.safe_parse(input, path: path, context: context)
44
+ ensure
45
+ active_refs.delete(key)
46
+ end
29
47
  end
30
48
 
31
49
  protected
@@ -67,8 +67,18 @@ module AnyVali
67
67
  end
68
68
 
69
69
  if @constraints["pattern"]
70
- re = Regexp.new(@constraints["pattern"])
71
- unless re.match?(value)
70
+ begin
71
+ re = Regexp.new(ecma_anchors(@constraints["pattern"]))
72
+ unless re.match?(value)
73
+ issues << ValidationIssue.new(
74
+ code: IssueCodes::INVALID_STRING,
75
+ path: path,
76
+ expected: @constraints["pattern"],
77
+ received: value
78
+ )
79
+ end
80
+ rescue RegexpError
81
+ # Invalid regex pattern - treat as validation failure
72
82
  issues << ValidationIssue.new(
73
83
  code: IssueCodes::INVALID_STRING,
74
84
  path: path,
@@ -109,5 +119,42 @@ module AnyVali
109
119
  Format::Validators.validate(value, @constraints["format"], path, issues)
110
120
  end
111
121
  end
122
+
123
+ private
124
+
125
+ # Rewrite ECMA-262 anchors to Ruby's absolute anchors. The spec (3.1) makes
126
+ # ECMA-262 the portable regex baseline, where "^"/"$" without the multiline
127
+ # flag match only the start/end of the whole string. In Ruby "^"/"$" are
128
+ # ALWAYS line anchors, so /^admin$/ matches "x\nadmin\ny" -- a line/newline
129
+ # injection bypass that diverges from the JS reference. Translate unescaped,
130
+ # top-level "^" -> "\A" and "$" -> "\z" (absolute start/end). Anchors inside
131
+ # character classes and escaped "\^"/"\$" are left untouched.
132
+ def ecma_anchors(pattern)
133
+ out = +""
134
+ escaped = false
135
+ in_class = false
136
+ pattern.each_char do |ch|
137
+ if escaped
138
+ out << ch
139
+ escaped = false
140
+ elsif ch == "\\"
141
+ out << ch
142
+ escaped = true
143
+ elsif ch == "["
144
+ in_class = true
145
+ out << ch
146
+ elsif ch == "]" && in_class
147
+ in_class = false
148
+ out << ch
149
+ elsif ch == "^" && !in_class
150
+ out << "\\A"
151
+ elsif ch == "$" && !in_class
152
+ out << "\\z"
153
+ else
154
+ out << ch
155
+ end
156
+ end
157
+ out
158
+ end
112
159
  end
113
160
  end
data/lib/anyvali.rb CHANGED
@@ -132,8 +132,13 @@ module AnyVali
132
132
  TupleSchema.new(elements: elements.flatten)
133
133
  end
134
134
 
135
- def object(properties:, required: [], unknown_keys: "reject")
136
- ObjectSchema.new(properties: properties, required: required, unknown_keys: unknown_keys)
135
+ def object(properties:, required: [], unknown_keys: nil)
136
+ ObjectSchema.new(
137
+ properties: properties,
138
+ required: required,
139
+ unknown_keys: unknown_keys || "strip",
140
+ unknown_keys_explicit: !unknown_keys.nil?
141
+ )
137
142
  end
138
143
 
139
144
  def record(values)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: anyvali
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - AnyVali Contributors
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-30 00:00:00.000000000 Z
11
+ date: 2026-06-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -38,6 +38,34 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '13.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: simplecov
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.22'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.22'
55
+ - !ruby/object:Gem::Dependency
56
+ name: simplecov-cobertura
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.1'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.1'
41
69
  description: AnyVali Ruby SDK - native validation with portable schema interchange
42
70
  across 10 languages
43
71
  email: