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 +4 -4
- data/README.md +40 -9
- data/lib/anyvali/interchange/importer.rb +9 -4
- data/lib/anyvali/parse/coercion.rb +5 -0
- data/lib/anyvali/schema.rb +72 -3
- data/lib/anyvali/schemas/number_schema.rb +20 -0
- data/lib/anyvali/schemas/object_schema.rb +18 -6
- data/lib/anyvali/schemas/ref_schema.rb +19 -1
- data/lib/anyvali/schemas/string_schema.rb +49 -2
- data/lib/anyvali.rb +7 -2
- metadata +30 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c865ca84583452f4064bf2789277cd979c33e32f718aa3ed3653e58de25e0158
|
|
4
|
+
data.tar.gz: 25de1308d962585be84d11b97830acbb5d399d2e401411e98fdd54ea7375a28d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
|
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 "
|
|
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 "
|
|
180
|
-
import { initForm } from "
|
|
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 "
|
|
202
|
-
import { createFormBindings } from "
|
|
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": "
|
|
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 | [
|
|
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
|
|
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"] || "
|
|
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
|
|
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 }
|
data/lib/anyvali/schema.rb
CHANGED
|
@@ -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
|
-
|
|
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: "
|
|
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"] =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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:
|
|
136
|
-
ObjectSchema.new(
|
|
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.
|
|
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-
|
|
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:
|