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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7c53ac42dbac09b5549467769f54d5597699feb351c0f77975cced4cb11bd569
4
+ data.tar.gz: c73e7af5ae8c388e30fe3e9040a93bbd212d0d1ad16a05761d8db2bb4fe0348f
5
+ SHA512:
6
+ metadata.gz: e3f43ec3aa8ff4310fd866208fadb24997c0304fb0cfda21ed8967d614b5695abd345ed63fc47dbedb39acfa5b97b183f61357c042ef723007d96ab43fb8a2e4
7
+ data.tar.gz: dbf001ffb2e24ac3d881fe4066b2f675f44f66f47123b684ab8828b8adf441f76c7d5b8665e91fce3939493e06b62e58f2a99902fda0a33869904b3daf640b59
data/README.md ADDED
@@ -0,0 +1,339 @@
1
+ <p align="center">
2
+ <img src="logo.png" alt="AnyVali" width="200" />
3
+ </p>
4
+
5
+ <h1 align="center">AnyVali</h1>
6
+
7
+ <p align="center">
8
+ <strong>Native validation libraries for 10 languages, one portable schema model.</strong>
9
+ </p>
10
+
11
+ <p align="center">
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://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://pypi.org/project/anyvali/"><img src="https://img.shields.io/pypi/v/anyvali.svg?label=pypi" alt="PyPI" /></a>
16
+ <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
+ <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>
18
+ <a href="https://www.nuget.org/packages/AnyVali"><img src="https://img.shields.io/nuget/v/AnyVali.svg?label=nuget" alt="NuGet" /></a>
19
+ <a href="https://rubygems.org/gems/anyvali"><img src="https://img.shields.io/gem/v/anyvali.svg?label=gem" alt="Gem" /></a>
20
+ </p>
21
+
22
+ <p align="center">
23
+ <a href="https://anyvali.com">Website</a> &middot;
24
+ <a href="https://docs.anyvali.com">Docs</a> &middot;
25
+ <a href="https://github.com/BetterCorp/AnyVali/issues">Issues</a> &middot;
26
+ <a href="CONTRIBUTING.md">Contributing</a>
27
+ </p>
28
+
29
+ ---
30
+
31
+ AnyVali lets you write validation schemas in your language, then share them across any of 10 supported runtimes via a portable JSON format. Think Zod, but for every language.
32
+
33
+ ## Why AnyVali?
34
+
35
+ - **Write schemas natively** -- idiomatic APIs for each language, not a separate DSL
36
+ - **Share across languages** -- export to JSON, import in any other SDK
37
+ - **Safe numeric defaults** -- `number` = float64, `int` = int64 everywhere
38
+ - **Deterministic parsing** -- coerce, default, then validate, in that order
39
+ - **Conformance tested** -- shared test corpus ensures identical behavior across SDKs
40
+
41
+ ## Install
42
+
43
+ ```bash
44
+ npm install @anyvali/js # JavaScript / TypeScript
45
+ pip install anyvali # Python
46
+ go get github.com/BetterCorp/AnyVali/sdk/go # Go
47
+ cargo add anyvali # Rust
48
+ dotnet add package AnyVali # C#
49
+ composer require anyvali/anyvali # PHP
50
+ gem install anyvali # Ruby
51
+ ```
52
+
53
+ <details>
54
+ <summary>Java / Kotlin / C++</summary>
55
+
56
+ **Java (Maven)**
57
+ ```xml
58
+ <dependency>
59
+ <groupId>com.anyvali</groupId>
60
+ <artifactId>anyvali</artifactId>
61
+ <version>0.0.1</version>
62
+ </dependency>
63
+ ```
64
+
65
+ **Kotlin (Gradle)**
66
+ ```kotlin
67
+ implementation("com.anyvali:anyvali:0.0.1")
68
+ ```
69
+
70
+ **C++ (CMake)**
71
+ ```cmake
72
+ FetchContent_Declare(anyvali GIT_REPOSITORY https://github.com/BetterCorp/AnyVali)
73
+ FetchContent_MakeAvailable(anyvali)
74
+ target_link_libraries(your_target PRIVATE anyvali)
75
+ ```
76
+ </details>
77
+
78
+ ## Quick Start
79
+
80
+ Define a schema, parse input, get structured errors or clean data.
81
+
82
+ <table>
83
+ <tr><th>JavaScript / TypeScript</th><th>Python</th></tr>
84
+ <tr>
85
+ <td>
86
+
87
+ ```typescript
88
+ import { string, int, object, array } from "@anyvali/js";
89
+
90
+ const User = object({
91
+ name: string().minLength(1),
92
+ email: string().format('email'),
93
+ age: int().min(0).optional(),
94
+ tags: array(string()).maxItems(5),
95
+ });
96
+
97
+ // Throws on failure
98
+ const user = User.parse(input);
99
+
100
+ // Or get a result object
101
+ const result = User.safeParse(input);
102
+ if (!result.success) {
103
+ console.log(result.issues);
104
+ }
105
+ ```
106
+
107
+ </td>
108
+ <td>
109
+
110
+ ```python
111
+ import anyvali as v
112
+
113
+ User = v.object_({
114
+ "name": v.string().min_length(1),
115
+ "email": v.string().format("email"),
116
+ "age": v.int_().min(0).optional(),
117
+ "tags": v.array(v.string()).max_items(5),
118
+ })
119
+
120
+ # Raises on failure
121
+ user = User.parse(input_data)
122
+
123
+ # Or get a result object
124
+ result = User.safe_parse(input_data)
125
+ if not result.success:
126
+ print(result.issues)
127
+ ```
128
+
129
+ </td>
130
+ </tr>
131
+ </table>
132
+
133
+ <details>
134
+ <summary>Go example</summary>
135
+
136
+ ```go
137
+ import av "github.com/BetterCorp/AnyVali/sdk/go"
138
+
139
+ User := av.Object(map[string]av.Schema{
140
+ "name": av.String().MinLength(1),
141
+ "email": av.String().Format("email"),
142
+ "age": av.Optional(av.Int().Min(0)),
143
+ "tags": av.Array(av.String()).MaxItems(5),
144
+ })
145
+
146
+ result := User.SafeParse(input)
147
+ if !result.Success {
148
+ for _, issue := range result.Issues {
149
+ fmt.Printf("[%s] %s at %v\n", issue.Code, issue.Message, issue.Path)
150
+ }
151
+ }
152
+ ```
153
+ </details>
154
+
155
+ ## Cross-Language Schema Sharing
156
+
157
+ AnyVali's core feature: export a schema from one language, import it in another.
158
+
159
+ ```typescript
160
+ // TypeScript frontend -- export
161
+ const doc = User.export();
162
+ const json = JSON.stringify(doc);
163
+ // Send to your backend, save to DB, put in a config file...
164
+ ```
165
+
166
+ ```python
167
+ # Python backend -- import
168
+ import json, anyvali as v
169
+
170
+ schema = v.import_schema(json.loads(schema_json))
171
+ result = schema.safe_parse(request_body) # Same validation rules!
172
+ ```
173
+
174
+ ## Forms
175
+
176
+ The JS SDK also ships a small forms layer for browser-native fields, HTML5 attributes, and AnyVali validation.
177
+
178
+ ```typescript
179
+ import { object, string, int } from "@anyvali/js";
180
+ import { initForm } from "@anyvali/js/forms";
181
+
182
+ const Signup = object({
183
+ email: string().format("email"),
184
+ age: int().min(18),
185
+ });
186
+
187
+ initForm("#signup", { schema: Signup });
188
+ ```
189
+
190
+ ```html
191
+ <form id="signup">
192
+ <input name="email" type="email" />
193
+ <input name="age" type="number" />
194
+ <button type="submit">Create account</button>
195
+ </form>
196
+ ```
197
+
198
+ For JSX-style attribute binding:
199
+
200
+ ```tsx
201
+ import { object, string } from "@anyvali/js";
202
+ import { createFormBindings } from "@anyvali/js/forms";
203
+
204
+ const Signup = object({
205
+ email: string().format("email"),
206
+ });
207
+
208
+ const form = createFormBindings({ schema: Signup });
209
+
210
+ <input {...form.field("email")} />;
211
+ ```
212
+
213
+ The portable JSON format:
214
+
215
+ ```json
216
+ {
217
+ "anyvaliVersion": "1.0",
218
+ "schemaVersion": "1",
219
+ "root": {
220
+ "kind": "object",
221
+ "properties": {
222
+ "name": { "kind": "string", "minLength": 1 },
223
+ "email": { "kind": "string", "format": "email" }
224
+ },
225
+ "required": ["name", "email"],
226
+ "unknownKeys": "reject"
227
+ },
228
+ "definitions": {},
229
+ "extensions": {}
230
+ }
231
+ ```
232
+
233
+ ## Supported SDKs
234
+
235
+ | Language | Package | Status |
236
+ |----------|---------|--------|
237
+ | JavaScript / TypeScript | [`@anyvali/js`](https://www.npmjs.com/package/@anyvali/js) | v0.0.1 |
238
+ | Python | [`anyvali`](https://pypi.org/project/anyvali/) | v0.0.1 |
239
+ | Go | [`github.com/BetterCorp/AnyVali/sdk/go`](https://pkg.go.dev/github.com/BetterCorp/AnyVali/sdk/go) | v0.0.1 |
240
+ | Java | `com.anyvali:anyvali` | v0.0.1 |
241
+ | C# | [`AnyVali`](https://www.nuget.org/packages/AnyVali) | v0.0.1 |
242
+ | Rust | [`anyvali`](https://crates.io/crates/anyvali) | v0.0.1 |
243
+ | PHP | `anyvali/anyvali` | v0.0.1 |
244
+ | Ruby | [`anyvali`](https://rubygems.org/gems/anyvali) | v0.0.1 |
245
+ | Kotlin | `com.anyvali:anyvali` | v0.0.1 |
246
+ | C++ | `anyvali` (CMake) | v0.0.1 |
247
+
248
+ ## CLI & HTTP API
249
+
250
+ Don't need an SDK? Use AnyVali from the command line or as a validation microservice.
251
+
252
+ ```bash
253
+ # Validate from the command line
254
+ anyvali validate schema.json '{"name": "Alice", "email": "alice@test.com"}'
255
+
256
+ # Pipe from stdin
257
+ cat payload.json | anyvali validate schema.json -
258
+
259
+ # Start a validation server
260
+ anyvali serve --port 8080 --schemas ./schemas/
261
+
262
+ # Validate via HTTP
263
+ curl -X POST http://localhost:8080/validate/user \
264
+ -H "Content-Type: application/json" \
265
+ -d '{"name": "Alice", "email": "alice@test.com"}'
266
+ ```
267
+
268
+ Pre-built binaries for Linux, macOS, and Windows are available on the [releases page](https://github.com/BetterCorp/AnyVali/releases). Docker image: `docker pull anyvali/cli`.
269
+
270
+ See the [CLI Reference](https://docs.anyvali.com/docs/cli) and [HTTP API Reference](https://docs.anyvali.com/docs/api) for full documentation.
271
+
272
+ ## Schema Types
273
+
274
+ | Category | Types |
275
+ |----------|-------|
276
+ | **Primitives** | `string`, `bool`, `null` |
277
+ | **Numbers** | `number` (float64), `int` (int64), `float32`, `float64`, `int8`-`int64`, `uint8`-`uint64` |
278
+ | **Special** | `any`, `unknown`, `never` |
279
+ | **Values** | `literal`, `enum` |
280
+ | **Collections** | `array`, `tuple`, `object`, `record` |
281
+ | **Composition** | `union`, `intersection` |
282
+ | **Modifiers** | `optional`, `nullable` |
283
+
284
+ ## Documentation
285
+
286
+ | Guide | Description |
287
+ |-------|-------------|
288
+ | [Getting Started](https://docs.anyvali.com) | Installation, API reference, examples |
289
+ | [Numeric Semantics](https://docs.anyvali.com/docs/numeric-semantics) | Why `number` = float64 and `int` = int64 |
290
+ | [Portability Guide](https://docs.anyvali.com/docs/portability-guide) | Design schemas that work across all languages |
291
+ | [SDK Authors Guide](https://docs.anyvali.com/docs/sdk-authors-guide) | Implement a new AnyVali SDK |
292
+ | [Canonical Spec](https://docs.anyvali.com/spec/spec) | The normative specification |
293
+ | [JSON Format](https://docs.anyvali.com/spec/json-format) | Interchange format details |
294
+ | [CLI Reference](https://docs.anyvali.com/docs/cli) | Command-line validation tool |
295
+ | [HTTP API](https://docs.anyvali.com/docs/api) | Validation microservice / sidecar |
296
+ | [Development](https://docs.anyvali.com/docs/development) | Building, testing, contributing |
297
+
298
+ ## Repository Layout
299
+
300
+ ```
301
+ .
302
+ ├── docs/ Documentation guides
303
+ ├── spec/ Canonical spec, JSON format, conformance corpus
304
+ ├── sdk/
305
+ │ ├── js/ JavaScript / TypeScript SDK
306
+ │ ├── python/ Python SDK
307
+ │ ├── go/ Go SDK
308
+ │ ├── java/ Java SDK
309
+ │ ├── csharp/ C# SDK
310
+ │ ├── rust/ Rust SDK
311
+ │ ├── php/ PHP SDK
312
+ │ ├── ruby/ Ruby SDK
313
+ │ ├── kotlin/ Kotlin SDK
314
+ │ └── cpp/ C++ SDK
315
+ ├── cli/ CLI binary and HTTP API server (Go)
316
+ ├── runner.sh Build/test/CI runner
317
+ └── site/ anyvali.com source
318
+ ```
319
+
320
+ ## Contributing
321
+
322
+ Contributions are welcome. Please read [CONTRIBUTING.md](CONTRIBUTING.md) before opening a pull request.
323
+
324
+ ```bash
325
+ ./runner.sh help # See all commands
326
+ ./runner.sh test js # Test a specific SDK
327
+ ./runner.sh ci # Run the full CI pipeline locally
328
+ pwsh -File tools/release/build_release.ps1 # Build release artifacts with Docker
329
+ ```
330
+
331
+ ## License
332
+
333
+ AnyVali is licensed under the [MIT License](LICENSE).
334
+
335
+ ---
336
+
337
+ <p align="center">
338
+ <a href="https://anyvali.com">anyvali.com</a>
339
+ </p>
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyVali
4
+ class AnyValiDocument
5
+ ANYVALI_VERSION = "1.0"
6
+ SCHEMA_VERSION = "1"
7
+
8
+ attr_reader :anyvali_version, :schema_version, :root, :definitions, :extensions
9
+
10
+ def initialize(root:, definitions: {}, extensions: {}, anyvali_version: ANYVALI_VERSION, schema_version: SCHEMA_VERSION)
11
+ @anyvali_version = anyvali_version
12
+ @schema_version = schema_version
13
+ @root = root
14
+ @definitions = definitions
15
+ @extensions = extensions
16
+ end
17
+
18
+ def to_h
19
+ {
20
+ "anyvaliVersion" => @anyvali_version,
21
+ "schemaVersion" => @schema_version,
22
+ "root" => node_to_h(@root),
23
+ "definitions" => @definitions.transform_values { |v| node_to_h(v) },
24
+ "extensions" => @extensions
25
+ }
26
+ end
27
+
28
+ def to_json(*_args)
29
+ require "json"
30
+ JSON.generate(to_h)
31
+ end
32
+
33
+ private
34
+
35
+ def node_to_h(node)
36
+ case node
37
+ when Hash then node
38
+ when Schema then node.to_node
39
+ else node
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+ require "ipaddr"
5
+ require "date"
6
+
7
+ module AnyVali
8
+ module Format
9
+ module Validators
10
+ EMAIL_REGEX = /\A[^@\s]+@[^@\s]+\.[^@\s]+\z/
11
+ UUID_REGEX = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i
12
+ # Strict IPv4: no leading zeros, octets 0-255
13
+ IPV4_REGEX = /\A(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)\z/
14
+ DATETIME_REGEX = /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})\z/
15
+ DATE_REGEX = /\A\d{4}-\d{2}-\d{2}\z/
16
+
17
+ module_function
18
+
19
+ def validate(value, format, path, issues)
20
+ valid = case format
21
+ when "email" then valid_email?(value)
22
+ when "url" then valid_url?(value)
23
+ when "uuid" then valid_uuid?(value)
24
+ when "ipv4" then valid_ipv4?(value)
25
+ when "ipv6" then valid_ipv6?(value)
26
+ when "date" then valid_date?(value)
27
+ when "date-time" then valid_datetime?(value)
28
+ else false
29
+ end
30
+
31
+ unless valid
32
+ issues << ValidationIssue.new(
33
+ code: IssueCodes::INVALID_STRING,
34
+ path: path,
35
+ expected: format,
36
+ received: value
37
+ )
38
+ end
39
+ end
40
+
41
+ def valid_email?(value)
42
+ EMAIL_REGEX.match?(value)
43
+ end
44
+
45
+ def valid_url?(value)
46
+ uri = URI.parse(value)
47
+ %w[http https].include?(uri.scheme) && !uri.host.nil?
48
+ rescue URI::InvalidURIError
49
+ false
50
+ end
51
+
52
+ def valid_uuid?(value)
53
+ UUID_REGEX.match?(value)
54
+ end
55
+
56
+ def valid_ipv4?(value)
57
+ # Must match strict pattern (no leading zeros)
58
+ return false unless IPV4_REGEX.match?(value)
59
+ # Check for leading zeros explicitly
60
+ parts = value.split(".")
61
+ parts.length == 4 && parts.all? { |p| p == "0" || !p.start_with?("0") }
62
+ end
63
+
64
+ def valid_ipv6?(value)
65
+ addr = IPAddr.new(value)
66
+ addr.ipv6?
67
+ rescue IPAddr::InvalidAddressError, IPAddr::AddressFamilyError
68
+ false
69
+ rescue ArgumentError
70
+ false
71
+ end
72
+
73
+ def valid_date?(value)
74
+ return false unless DATE_REGEX.match?(value)
75
+ Date.parse(value)
76
+ true
77
+ rescue Date::Error, ArgumentError
78
+ false
79
+ end
80
+
81
+ def valid_datetime?(value)
82
+ return false unless DATETIME_REGEX.match?(value)
83
+ # Validate the date part
84
+ date_part = value[0..9]
85
+ valid_date?(date_part)
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module AnyVali
6
+ module Interchange
7
+ module Exporter
8
+ module_function
9
+
10
+ def export(schema, mode: :portable, definitions: {})
11
+ if mode == :portable && !schema.portable?
12
+ raise ValidationError, [
13
+ ValidationIssue.new(
14
+ code: IssueCodes::CUSTOM_VALIDATION_NOT_PORTABLE,
15
+ expected: "portable schema",
16
+ received: "schema with custom validators"
17
+ )
18
+ ]
19
+ end
20
+
21
+ doc = {
22
+ "anyvaliVersion" => AnyValiDocument::ANYVALI_VERSION,
23
+ "schemaVersion" => AnyValiDocument::SCHEMA_VERSION,
24
+ "root" => schema_to_node(schema),
25
+ "definitions" => definitions.transform_values { |v| schema_to_node(v) },
26
+ "extensions" => {}
27
+ }
28
+ doc
29
+ end
30
+
31
+ def to_json(schema, mode: :portable, definitions: {})
32
+ JSON.generate(export(schema, mode: mode, definitions: definitions))
33
+ end
34
+
35
+ def schema_to_node(schema)
36
+ schema.to_node
37
+ end
38
+ end
39
+ end
40
+ end