philiprehberger-json_schema 0.2.3 → 0.3.0

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: 143f39a54828b31d7a745796701be504fc920b45bf6854216d12310fedcc1a18
4
- data.tar.gz: 662b04162211c5b5d85efb5a0b378c21da0f54457866485c94e3760388662d29
3
+ metadata.gz: 2a67bbf9af3550800b697cffb7061d297e1cc4d4d658b1db1f4c3eb3133c5125
4
+ data.tar.gz: a805669bc1f806de5fd76ac8ff9b28414676b2772ef71c67bdbad40ba5ebf66d
5
5
  SHA512:
6
- metadata.gz: b9b492cac2a6bfa9d117979d5ba899b41b88c187373f99073cee4d46ab2a5b8c0e3e2de340ea68d40538b99f54c65a240b8ff04703d5a4a885076cb993c991eb
7
- data.tar.gz: b18f7bef9389eae54ba47f7c5caf3d2c6de778a7923ff2bd0e14838b86861d25bca489fada64f73606c490f740769d75d263ee2cefb32e3cdc69669f57cbd9da
6
+ metadata.gz: f14027b317fc2d9b1f36dd79351e5da1ef2b43ebe14a00877cff83221db2e2e63d5802f5142389147285fd020992d5338583850947edb49f244383157710aa17
7
+ data.tar.gz: 4290e58110711d0ad8e9ed236d4d7dbea255621ca7dd985a48e63fe2c20b100545fbca3b5437406653fd448a54b74f44cfcdbb45a4ed22ffcd1c4f49b8fad3fe
data/CHANGELOG.md CHANGED
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.0] - 2026-04-17
11
+
12
+ ### Added
13
+ - `exclusiveMinimum` and `exclusiveMaximum` keywords for strict numeric bounds
14
+ - `multipleOf` keyword, using BigDecimal arithmetic to avoid IEEE-754 drift on fractional multiples
15
+ - `uniqueItems` keyword for arrays (deep equality on nested values)
16
+ - `minProperties` and `maxProperties` keywords for object property-count constraints
17
+ - README: Usage examples for the new keywords and expanded Supported Keywords table
18
+
10
19
  ## [0.2.3] - 2026-04-09
11
20
 
12
21
  ### Fixed
data/README.md CHANGED
@@ -141,6 +141,39 @@ Philiprehberger::JsonSchema.valid?('fixed_value', schema) # => true
141
141
  Philiprehberger::JsonSchema.valid?('other', schema) # => false
142
142
  ```
143
143
 
144
+ ### Numeric Bounds (exclusive and multipleOf)
145
+
146
+ ```ruby
147
+ # exclusive bounds
148
+ schema = { type: 'integer', exclusiveMinimum: 0, exclusiveMaximum: 10 }
149
+ Philiprehberger::JsonSchema.valid?(5, schema) # => true
150
+ Philiprehberger::JsonSchema.valid?(0, schema) # => false (equal to exclusiveMinimum)
151
+ Philiprehberger::JsonSchema.valid?(10, schema) # => false (equal to exclusiveMaximum)
152
+
153
+ # multipleOf (BigDecimal-backed, safe for fractions)
154
+ schema = { type: 'number', multipleOf: 0.1 }
155
+ Philiprehberger::JsonSchema.valid?(0.3, schema) # => true
156
+ Philiprehberger::JsonSchema.valid?(0.25, schema) # => false
157
+ ```
158
+
159
+ ### Unique Array Items
160
+
161
+ ```ruby
162
+ schema = { type: 'array', uniqueItems: true }
163
+ Philiprehberger::JsonSchema.valid?([1, 2, 3], schema) # => true
164
+ Philiprehberger::JsonSchema.valid?([1, 2, 2], schema) # => false
165
+ Philiprehberger::JsonSchema.valid?([{ 'a' => 1 }, { 'a' => 1 }], schema) # => false (deep equality)
166
+ ```
167
+
168
+ ### Object Property Counts
169
+
170
+ ```ruby
171
+ schema = { type: 'object', minProperties: 1, maxProperties: 3 }
172
+ Philiprehberger::JsonSchema.valid?({ 'a' => 1 }, schema) # => true
173
+ Philiprehberger::JsonSchema.valid?({}, schema) # => false
174
+ Philiprehberger::JsonSchema.valid?({ 'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4 }, schema) # => false
175
+ ```
176
+
144
177
  ### Compiled Schemas
145
178
 
146
179
  ```ruby
@@ -182,11 +215,15 @@ compiled.validate({ 'id' => 'x' }) # => ["$.id: expected type integer..."]
182
215
  | `patternProperties` | Regex-keyed sub-schemas for matching property names |
183
216
  | `pattern` | Regex match for strings |
184
217
  | `minLength` / `maxLength` | String length constraints |
185
- | `minimum` / `maximum` | Numeric range constraints |
218
+ | `minimum` / `maximum` | Numeric range constraints (inclusive) |
219
+ | `exclusiveMinimum` / `exclusiveMaximum` | Numeric range constraints (exclusive) |
220
+ | `multipleOf` | Value must be a multiple of the given number (BigDecimal-backed) |
186
221
  | `enum` | Allowed value list |
187
222
  | `const` | Exact value match |
188
223
  | `items` | Schema for array elements |
189
224
  | `minItems` / `maxItems` | Array length constraints |
225
+ | `uniqueItems` | Require all array items to be unique (deep equality) |
226
+ | `minProperties` / `maxProperties` | Object property-count constraints |
190
227
  | `allOf` | Data must match all sub-schemas |
191
228
  | `anyOf` | Data must match at least one sub-schema |
192
229
  | `oneOf` | Data must match exactly one sub-schema |
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'bigdecimal'
4
+
3
5
  module Philiprehberger
4
6
  module JsonSchema
5
7
  # Core validation engine that checks data against a JSON Schema (draft-07 subset)
@@ -18,15 +20,19 @@ module Philiprehberger
18
20
  validate_type(data, schema, path, errors)
19
21
  validate_const(data, schema, path, errors)
20
22
  validate_required(data, schema, path, errors)
23
+ validate_object_size(data, schema, path, errors)
21
24
  validate_properties(data, schema, path, errors, root_schema)
22
25
  validate_additional_properties(data, schema, path, errors, root_schema)
23
26
  validate_pattern_properties(data, schema, path, errors, root_schema)
24
27
  validate_pattern(data, schema, path, errors)
25
28
  validate_string_length(data, schema, path, errors)
26
29
  validate_numeric_range(data, schema, path, errors)
30
+ validate_exclusive_range(data, schema, path, errors)
31
+ validate_multiple_of(data, schema, path, errors)
27
32
  validate_enum(data, schema, path, errors)
28
33
  validate_items(data, schema, path, errors, root_schema)
29
34
  validate_array_length(data, schema, path, errors)
35
+ validate_unique_items(data, schema, path, errors)
30
36
  validate_ref(data, schema, path, errors, root_schema)
31
37
  validate_all_of(data, schema, path, errors, root_schema)
32
38
  validate_any_of(data, schema, path, errors, root_schema)
@@ -176,6 +182,31 @@ module Philiprehberger
176
182
  errors << "#{path}: value #{data} is greater than maximum #{max}" if max && data > max
177
183
  end
178
184
 
185
+ def validate_exclusive_range(data, schema, path, errors)
186
+ return unless data.is_a?(Numeric) && !data.is_a?(Complex)
187
+
188
+ excl_min = schema_key(schema, :exclusiveMinimum)
189
+ excl_max = schema_key(schema, :exclusiveMaximum)
190
+
191
+ min_violated = excl_min.is_a?(Numeric) && data <= excl_min
192
+ max_violated = excl_max.is_a?(Numeric) && data >= excl_max
193
+
194
+ errors << "#{path}: value #{data} is not greater than exclusiveMinimum #{excl_min}" if min_violated
195
+ errors << "#{path}: value #{data} is not less than exclusiveMaximum #{excl_max}" if max_violated
196
+ end
197
+
198
+ def validate_multiple_of(data, schema, path, errors)
199
+ return unless data.is_a?(Numeric) && !data.is_a?(Complex)
200
+
201
+ multiple = schema_key(schema, :multipleOf)
202
+ return unless multiple.is_a?(Numeric) && multiple.positive? && multiple.finite?
203
+
204
+ # Use BigDecimal via string representation to sidestep IEEE-754 drift
205
+ # (e.g. 0.3 % 0.1 != 0 in Float arithmetic).
206
+ remainder = BigDecimal(data.to_s) % BigDecimal(multiple.to_s)
207
+ errors << "#{path}: value #{data} is not a multiple of #{multiple}" unless remainder.zero?
208
+ end
209
+
179
210
  def validate_enum(data, schema, path, errors)
180
211
  allowed = schema_key(schema, :enum)
181
212
  return unless allowed.is_a?(Array)
@@ -204,6 +235,29 @@ module Philiprehberger
204
235
  errors << "#{path}: array length #{data.length} is greater than maxItems #{max}" if max && data.length > max
205
236
  end
206
237
 
238
+ def validate_unique_items(data, schema, path, errors)
239
+ return unless data.is_a?(Array)
240
+
241
+ unique = schema_key(schema, :uniqueItems)
242
+ return unless unique == true
243
+
244
+ errors << "#{path}: array items are not unique" if data.length != data.uniq.length
245
+ end
246
+
247
+ def validate_object_size(data, schema, path, errors)
248
+ return unless data.is_a?(Hash)
249
+
250
+ min = schema_key(schema, :minProperties)
251
+ max = schema_key(schema, :maxProperties)
252
+ size = data.size
253
+
254
+ under_min = min.is_a?(Integer) && size < min
255
+ over_max = max.is_a?(Integer) && size > max
256
+
257
+ errors << "#{path}: object has #{size} properties, expected at least #{min}" if under_min
258
+ errors << "#{path}: object has #{size} properties, expected at most #{max}" if over_max
259
+ end
260
+
207
261
  def validate_ref(data, schema, path, errors, root_schema)
208
262
  ref = schema_key(schema, '$ref')
209
263
  return unless ref.is_a?(String)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module JsonSchema
5
- VERSION = '0.2.3'
5
+ VERSION = '0.3.0'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: philiprehberger-json_schema
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Philip Rehberger
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-09 00:00:00.000000000 Z
11
+ date: 2026-04-17 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Validate Ruby data structures against JSON Schema definitions with support
14
14
  for type checking, required properties, pattern matching, numeric ranges, enums,