yeptris 0.6.57.1 → 0.6.59.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8ca19dc06edfa2c51b8d193845713970526fb9c933a9d4ff438b4536e6847ff8
4
- data.tar.gz: 2d32ef5b5b0702e0d24accf88ddad7ec35dc922e33277037332d675b6c3316d7
3
+ metadata.gz: 5f9da8cd9b7db4c514ad70beefc657e82c11464804ef6c6e9a451462c7d4831c
4
+ data.tar.gz: 6f94ccb3a3499f4c71dc24c00231309e6da9cef893d8bdcea60da2d7b2dce456
5
5
  SHA512:
6
- metadata.gz: 79953b07f1bce4b68ca9d8a7124cbc11313ddb56f067291f9a8f3979f8cb9aee59c3af0370f076474d8abe64253e1c757fa04df8c8b7d236fec41e7c97cc3e7e
7
- data.tar.gz: 71668f985884d66d6828c50d854caf079f0f1c2dd40d7ecaf7fd5d49257b9f754f64d8d66296e3df6836d589e2b7ce36e9df36e2a17ce7b167c02d7e6c13d68b
6
+ metadata.gz: 7af7add33be58c2f972314fa3a235c83b455036ae74db7367a000a11ae13851d723955b770b63192aa68f54883f4fe5a15465c12547599afdb09d7c9231f5aef
7
+ data.tar.gz: feabebeaeb648b9810921337daf3466b85047b85f01072cad817277662bdfa5250fd40d4597b810abc769a5bf2ca1d5f299b1778130f5778bc85f2e2b2883964
@@ -238,6 +238,21 @@ class Yeptris::Document
238
238
  r.nil? ? nil : r.to_ruby
239
239
  end
240
240
 
241
+ # The schema-family validation walk (validate.h, #882) over this
242
+ # document — or over every document of the stream it holds (the
243
+ # engine's report is flat across the stream; paths are root-relative
244
+ # per document). schema: :core_12 or :json_12 (the validation
245
+ # families; :compat_11 is a resolve schema, not a validation
246
+ # family). Returns plain Yeptris::Validation::Diagnostic values
247
+ # (empty = valid).
248
+ def validate(schema: :core_12)
249
+ ensure_alive!
250
+ report = ::FFI::MemoryPointer.new(:pointer)
251
+ v = ::Yeptris::Validation
252
+ st = Yeptris::FFI.yeptris_validate_document(@c_ptr, v.schema_id(schema), report)
253
+ v.consume_report(st, report, "validate document")
254
+ end
255
+
241
256
  # ---- construction conveniences (TODO.impl/11 phase 3) ----
242
257
 
243
258
  def new_mapping
data/lib/yeptris/ffi.rb CHANGED
@@ -427,6 +427,9 @@ module Yeptris
427
427
 
428
428
  SCHEMA_12_CORE = 0
429
429
  SCHEMA_11_COMPAT = 1
430
+ # resolve.h's third kind (spec 10.2): plain scalars must match a
431
+ # JSON production; the validate face's schema-family walk rides it.
432
+ SCHEMA_12_JSON = 2
430
433
 
431
434
  OK = 0
432
435
  ERROR_PARSE = 1
@@ -437,6 +440,96 @@ module Yeptris
437
440
  ERROR_ARG = 6
438
441
  ERROR_UNSUPPORTED = 7
439
442
  ERROR_INTERNAL = 8
443
+
444
+ # ---- the validation face (validate.h; #882, engine 0.6.59+) ----
445
+ # Whole reports: one call produces a report the caller drains
446
+ # (count/at) and frees once. SCHEMA_ERROR (9, defined above) means
447
+ # the walk completed and the instance is INVALID — drain the
448
+ # report. Absent on older libraries: the attachment helper records
449
+ # the miss and VALIDATION gates the Ruby face with the friendly
450
+ # error at use.
451
+
452
+ typedef :pointer, :yeptris_schema
453
+ typedef :pointer, :yeptris_validate_report
454
+
455
+ # yeptris_diagnostic (validate.h): ABI-frozen — four uint32 facts
456
+ # then two bounded NUL-terminated texts. 16 + 256 + 128 bytes.
457
+ class CDiagnostic < ::FFI::Struct
458
+ layout :code, :uint32,
459
+ :line, :uint32,
460
+ :col, :uint32,
461
+ :offset, :uint32,
462
+ :path, [:char, 256],
463
+ :detail, [:char, 128]
464
+ end
465
+
466
+ # The schema-family walk: parse + validate fused (tag consistency,
467
+ # canonical-form key uniqueness; the JSON family's string-key +
468
+ # JSON-production rules under SCHEMA_12_JSON).
469
+ attach_function :yeptris_validate, %i[pointer size_t int pointer], :int
470
+ # An already-parsed document (or stream) against the family walk.
471
+ attach_function :yeptris_validate_document, %i[yeptris_document int pointer], :int
472
+ # JSON Schema draft 2020-12 subset, one-shot (schema bytes parsed
473
+ # per call).
474
+ attach_function :yeptris_json_schema_validate,
475
+ %i[pointer size_t pointer size_t pointer], :int
476
+ # The compiled-schema economy (#885 item 3): parse once, validate
477
+ # many. NULL from compile = failure (yeptris_last_error carries why).
478
+ attach_function :yeptris_schema_compile, %i[pointer size_t], :yeptris_schema
479
+ attach_function :yeptris_schema_validate,
480
+ %i[yeptris_schema pointer size_t pointer], :int
481
+ attach_function :yeptris_schema_free, [:yeptris_schema], :void
482
+ attach_function :yeptris_validate_report_count,
483
+ [:yeptris_validate_report], :size_t
484
+ attach_function :yeptris_validate_report_at,
485
+ %i[yeptris_validate_report size_t], :pointer
486
+ attach_function :yeptris_validate_report_free,
487
+ [:yeptris_validate_report], :void
488
+
489
+ # The diag codes (validate.h's YeptrisDiagCode, appending-only;
490
+ # pinned values — the engine's test_abi pins the head).
491
+ DIAG_NONE = 0
492
+ DIAG_TAG_MISMATCH = 1
493
+ DIAG_KEY_NOT_STRING = 2
494
+ DIAG_NON_JSON_TYPE = 3
495
+ DIAG_DUPLICATE_KEY = 4
496
+ DIAG_TYPE = 5
497
+ DIAG_REQUIRED = 6
498
+ DIAG_ENUM = 7
499
+ DIAG_CONST = 8
500
+ DIAG_PATTERN = 9
501
+ DIAG_MINIMUM = 10
502
+ DIAG_MAXIMUM = 11
503
+ DIAG_EXCLUSIVE_MINIMUM = 12
504
+ DIAG_EXCLUSIVE_MAXIMUM = 13
505
+ DIAG_MIN_LENGTH = 14
506
+ DIAG_MAX_LENGTH = 15
507
+ DIAG_MIN_ITEMS = 16
508
+ DIAG_MAX_ITEMS = 17
509
+ DIAG_UNIQUE_ITEMS = 18
510
+ DIAG_ADDITIONAL_PROPERTY = 19
511
+ DIAG_ANY_OF = 20
512
+ DIAG_ONE_OF = 21
513
+ DIAG_NOT = 22
514
+ DIAG_REF = 23
515
+ DIAG_FALSE_SCHEMA = 24
516
+ DIAG_DEPTH = 25
517
+ DIAG_LIMIT = 26
518
+ DIAG_MULTIPLE_OF = 27
519
+ DIAG_MIN_PROPERTIES = 28
520
+ DIAG_MAX_PROPERTIES = 29
521
+ DIAG_CONTAINS = 30
522
+ DIAG_MIN_CONTAINS = 31
523
+ DIAG_MAX_CONTAINS = 32
524
+ DIAG_DEPENDENT_REQUIRED = 33
525
+
526
+ # The face's availability (engine 0.6.59+); the Validation module
527
+ # raises the friendly error at use when the loaded library predates
528
+ # the attachments (the stubs above would raise NotFoundError raw).
529
+ VALIDATION = !(@missing ||= []).include?(:yeptris_validate) &&
530
+ !(@missing ||= []).include?(:yeptris_schema_compile) &&
531
+ !(@missing ||= []).include?(:yeptris_validate_report_at)
532
+
440
533
  # The load-time essentials (the read path + the ownership
441
534
  # contract): absent on engines far older than the optional
442
535
  # surfaces — raise the friendly error once every constant has
@@ -13,12 +13,17 @@ module Yeptris
13
13
  # !ruby/... tags Psych's own emitter produces.
14
14
  class YAMLTree
15
15
  # stdlib's factory (relaton's db cache calls it): the options
16
- # ride our visitor defaults
16
+ # reach the engine's emitter — :line_width and :canonical ride
17
+ # yeptris_emit_options (13B), :indentation is accepted for
18
+ # Psych parity (the engine's YAML emitter carries no
19
+ # indentation knob; emit.h's struct is size/canonical/
20
+ # best_width/explicit_doc_start).
17
21
  def self.create(options = {}, emitter = nil)
18
- new
22
+ new(options: options)
19
23
  end
20
24
 
21
- def initialize(schema: nil)
25
+ def initialize(schema: nil, options: {})
26
+ @options = options || {}
22
27
  @tree = ::Yeptris::Document.create(schema: schema)
23
28
  @names = {} # object_id -> anchor name
24
29
  @counter = 0
@@ -48,7 +53,9 @@ module Yeptris
48
53
  end
49
54
 
50
55
  def finish
51
- @tree.serialize(explicit_doc_start: true)
56
+ @tree.serialize(canonical: @options[:canonical] ? true : false,
57
+ best_width: @options[:line_width] || 0,
58
+ explicit_doc_start: true)
52
59
  end
53
60
 
54
61
  # The built-document handle stdlib's dump tail serializes
@@ -57,8 +64,11 @@ module Yeptris
57
64
  @doc = doc
58
65
  end
59
66
 
60
- def yaml(io = nil, _options = {})
61
- out = @doc.serialize(explicit_doc_start: true)
67
+ def yaml(io = nil, options = {})
68
+ options ||= {}
69
+ out = @doc.serialize(canonical: options[:canonical] ? true : false,
70
+ best_width: options[:line_width] || 0,
71
+ explicit_doc_start: true)
62
72
  return out unless io
63
73
 
64
74
  io.write(out)
data/lib/yeptris/psych.rb CHANGED
@@ -424,11 +424,16 @@ module Yeptris
424
424
 
425
425
  # Arbitrary object graphs through the YAMLTree visitor
426
426
  # (anchors, !ruby/ tags); plain data rides the fast builder.
427
+ # options reach the engine's emitter: :line_width folds flow
428
+ # collections past the width (the engine default is 80),
429
+ # :canonical switches the fixed form; :indentation is accepted
430
+ # for Psych parity (the engine's YAML emitter has no indentation
431
+ # knob). The audit round-2 finding — width accepted and
432
+ # discarded — is closed here and in YAMLTree.create.
427
433
  def dump(obj, io = nil, options = {})
428
434
  # stdlib's Object#to_yaml calls Psych.dump(self, options) —
429
435
  # the options hash lands in the io slot positionally; treat a
430
- # Hash there as options (the options themselves ride the
431
- # visitor's defaults, #95 thread)
436
+ # Hash there as options
432
437
  if io.is_a?(::Hash)
433
438
  options = io
434
439
  io = nil
@@ -443,11 +448,12 @@ module Yeptris
443
448
  # path would emit it tag-less iso8601 (re-loading as Time);
444
449
  # the visitor carries stdlib's !ruby/object:DateTime tag
445
450
  when ::DateTime
446
- Visitors::YAMLTree.new.push(obj).finish
451
+ Visitors::YAMLTree.new(options: options).push(obj).finish
447
452
  when nil, true, false, ::String, ::Integer, ::Float, ::Symbol, ::Date, ::Time
448
- Yeptris::YAML.dump(obj, header: true)
453
+ Yeptris::YAML.dump(obj, header: true, width: options[:line_width],
454
+ canonical: options[:canonical] ? true : false)
449
455
  else
450
- Visitors::YAMLTree.new.push(obj).finish
456
+ Visitors::YAMLTree.new(options: options).push(obj).finish
451
457
  end
452
458
  return out unless io
453
459
 
@@ -0,0 +1,320 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yeptris
4
+ # The validation face (engine validate.h; issue #882, binding
5
+ # TODO item 6). Two checks over one engine pass:
6
+ #
7
+ # * the SCHEMA-FAMILY walk (`validate` with a schema symbol) —
8
+ # explicit-tag consistency, mapping-key uniqueness by canonical
9
+ # form, and (under :json_12) the spec 10.2 JSON-production rules;
10
+ # * JSON SCHEMA draft 2020-12 (`validate` with `json_schema:`) —
11
+ # one-shot over schema bytes, or against a compiled
12
+ # {Yeptris::Validation::Schema} handle (parse once, validate
13
+ # many; #885 item 3).
14
+ #
15
+ # Diagnostics cross the FFI boundary once, as plain Ruby values
16
+ # ({Diagnostic}): no C pointer ever leaks out of this module.
17
+ module Validation
18
+ # Raised for an instance that carries diagnostics. The full report
19
+ # rides `diagnostics` (plain {Diagnostic} values, never pointers).
20
+ class Error < ::Yeptris::Error
21
+ attr_reader :diagnostics
22
+
23
+ def initialize(diagnostics)
24
+ @diagnostics = diagnostics
25
+ first = diagnostics.first
26
+ detail = first ? "#{first.keyword} at #{first.path}: #{first.detail}" : "no diagnostics"
27
+ super("validation failed (#{diagnostics.size} diagnostics): #{detail}")
28
+ end
29
+ end
30
+
31
+ # The code -> keyword table (validate.h's YeptrisDiagCode). The
32
+ # 2020-12 walk's codes name their JSON Schema keyword; the
33
+ # schema-family walk's codes name the violated YAML rule.
34
+ KEYWORD = {
35
+ FFI::DIAG_NONE => "none",
36
+ FFI::DIAG_TAG_MISMATCH => "tag-mismatch",
37
+ FFI::DIAG_KEY_NOT_STRING => "key-not-string",
38
+ FFI::DIAG_NON_JSON_TYPE => "non-json-type",
39
+ FFI::DIAG_DUPLICATE_KEY => "duplicate-key",
40
+ FFI::DIAG_TYPE => "type",
41
+ FFI::DIAG_REQUIRED => "required",
42
+ FFI::DIAG_ENUM => "enum",
43
+ FFI::DIAG_CONST => "const",
44
+ FFI::DIAG_PATTERN => "pattern",
45
+ FFI::DIAG_MINIMUM => "minimum",
46
+ FFI::DIAG_MAXIMUM => "maximum",
47
+ FFI::DIAG_EXCLUSIVE_MINIMUM => "exclusiveMinimum",
48
+ FFI::DIAG_EXCLUSIVE_MAXIMUM => "exclusiveMaximum",
49
+ FFI::DIAG_MIN_LENGTH => "minLength",
50
+ FFI::DIAG_MAX_LENGTH => "maxLength",
51
+ FFI::DIAG_MIN_ITEMS => "minItems",
52
+ FFI::DIAG_MAX_ITEMS => "maxItems",
53
+ FFI::DIAG_UNIQUE_ITEMS => "uniqueItems",
54
+ FFI::DIAG_ADDITIONAL_PROPERTY => "additionalProperties",
55
+ FFI::DIAG_ANY_OF => "anyOf",
56
+ FFI::DIAG_ONE_OF => "oneOf",
57
+ FFI::DIAG_NOT => "not",
58
+ FFI::DIAG_REF => "$ref",
59
+ FFI::DIAG_FALSE_SCHEMA => "false",
60
+ FFI::DIAG_DEPTH => "depth",
61
+ FFI::DIAG_LIMIT => "limit",
62
+ FFI::DIAG_MULTIPLE_OF => "multipleOf",
63
+ FFI::DIAG_MIN_PROPERTIES => "minProperties",
64
+ FFI::DIAG_MAX_PROPERTIES => "maxProperties",
65
+ FFI::DIAG_CONTAINS => "contains",
66
+ FFI::DIAG_MIN_CONTAINS => "minContains",
67
+ FFI::DIAG_MAX_CONTAINS => "maxContains",
68
+ FFI::DIAG_DEPENDENT_REQUIRED => "dependentRequired",
69
+ }.freeze
70
+
71
+ # One structured diagnostic: the engine's yeptris_diagnostic read
72
+ # into plain Ruby at drain time (a value class — ==, eql?, hash —
73
+ # never an FFI wrapper).
74
+ class Diagnostic
75
+ attr_reader :code, :path, :detail, :line, :col, :offset
76
+
77
+ def initialize(code:, path:, detail:, line: 0, col: 0, offset: 0)
78
+ @code = code
79
+ @path = path
80
+ @detail = detail
81
+ @line = line
82
+ @col = col
83
+ @offset = offset
84
+ end
85
+
86
+ # The violated keyword ("type", "minimum", "duplicate-key", ...).
87
+ def keyword
88
+ KEYWORD.fetch(code, "unknown-#{code}")
89
+ end
90
+
91
+ # The JSON-pointer instance path ("/a/b/0", ~0/~1 escaped) split
92
+ # into its unescaped segments; the root is [].
93
+ def path_array
94
+ return [] if path.empty?
95
+
96
+ path.split("/")[1..].map { |s| s.gsub("~1", "/").gsub("~0", "~") }
97
+ end
98
+
99
+ # The engine's bounded explanation ("3 is less than the minimum 10").
100
+ def message
101
+ detail
102
+ end
103
+
104
+ alias to_s message
105
+
106
+ def ==(other)
107
+ other.is_a?(Diagnostic) && code == other.code && path == other.path &&
108
+ detail == other.detail && line == other.line && col == other.col &&
109
+ offset == other.offset
110
+ end
111
+ alias eql? ==
112
+
113
+ def hash
114
+ [code, path, detail, line, col, offset].hash
115
+ end
116
+
117
+ def to_h
118
+ { code: code, keyword: keyword, path: path, path_array: path_array,
119
+ detail: detail, message: message, line: line, col: col, offset: offset }
120
+ end
121
+
122
+ def inspect
123
+ "#<#{self.class.name} #{keyword} path=#{path.inspect} line=#{line} " \
124
+ "col=#{col} detail=#{detail.inspect}>"
125
+ end
126
+ end
127
+
128
+ # The compiled schema (yeptris_schema_compile): the schema bytes,
129
+ # its DOM, and the pattern kernel's programs retained behind one
130
+ # opaque handle — parse ONCE, validate many streams against it.
131
+ # Ownership mirrors Yeptris::Document: an explicit #free and a GC
132
+ # finalizer flip the same freed flag (the pointer-only finalizer —
133
+ # it captures the raw address, never the wrapper, so no
134
+ # use-after-free race).
135
+ class Schema
136
+ # Shared between the instance and its finalizer (the
137
+ # leptris-ruby double-free fix).
138
+ Freed = Struct.new(:state) # :alive | :freed
139
+
140
+ attr_reader :c_ptr
141
+
142
+ # Compiles JSON Schema bytes (JSON is YAML flow — a YAML-shaped
143
+ # mapping schema compiles too). Raises ArgumentError for empty
144
+ # bytes (the engine's "hold no document") and ParseError for
145
+ # bytes that do not parse.
146
+ def self.compile(bytes)
147
+ raise ::ArgumentError, "schema bytes are required" if bytes.nil?
148
+ bytes = ::Yeptris.read_input(bytes).to_s
149
+ raise ::ArgumentError, "schema bytes hold no document" if bytes.empty?
150
+
151
+ ptr = FFI.yeptris_schema_compile(bytes, bytes.bytesize)
152
+ raise ::Yeptris::ParseError,
153
+ "schema compile failed: #{FFI.last_error_message}" if ptr.null?
154
+
155
+ new(ptr)
156
+ end
157
+
158
+ # Block form: the handle frees when the block exits (valid or not).
159
+ def self.open(bytes)
160
+ schema = compile(bytes)
161
+ begin
162
+ yield schema
163
+ ensure
164
+ schema.free
165
+ end
166
+ end
167
+
168
+ # @api private — produced by .compile; a raw pointer argument is
169
+ # not a caller surface (the same contract as Document).
170
+ def initialize(c_ptr)
171
+ unless c_ptr.is_a?(::FFI::Pointer)
172
+ raise ArgumentError,
173
+ "a schema FFI pointer is required — build schemas with " \
174
+ "Yeptris::Validation::Schema.compile"
175
+ end
176
+
177
+ @c_ptr = c_ptr
178
+ @freed = Freed.new(:alive)
179
+ ObjectSpace.define_finalizer(self, self.class.finalize(c_ptr)) unless c_ptr.null?
180
+ end
181
+
182
+ # Validates one instance stream against the retained model.
183
+ # Returns the diagnostics array (empty = valid).
184
+ def validate(bytes)
185
+ ensure_alive!
186
+ bytes = ::Yeptris.read_input(bytes).to_s
187
+ report = ::FFI::MemoryPointer.new(:pointer)
188
+ st = FFI.yeptris_schema_validate(@c_ptr, bytes, bytes.bytesize, report)
189
+ Validation.consume_report(st, report, "schema validate")
190
+ end
191
+
192
+ def valid?(bytes)
193
+ validate(bytes).empty?
194
+ end
195
+
196
+ def ensure_alive!
197
+ raise ::Yeptris::FreedError, "schema is freed" if @freed.state == :freed
198
+ end
199
+
200
+ def free
201
+ return if @freed.state == :freed
202
+
203
+ @freed.state = :freed
204
+ ObjectSpace.undefine_finalizer(self)
205
+ FFI.yeptris_schema_free(@c_ptr)
206
+ end
207
+
208
+ def freed?
209
+ @freed.state == :freed
210
+ end
211
+
212
+ # Pointer-only finalizer (Document's law): no Ruby objects, no
213
+ # wrapper — a finalizer closing over the wrapper races its sweep.
214
+ def self.finalize(c_ptr)
215
+ proc { FFI.yeptris_schema_free(c_ptr) }
216
+ end
217
+ end
218
+
219
+ class << self
220
+ # The schema-family walk over the engine's own front door
221
+ # (encodings, BOM, every document of the stream). schema: one of
222
+ # :core_12, :compat_11, :json_12. Returns the diagnostics array.
223
+ def validate_family(bytes, schema: :core_12)
224
+ require_engine!
225
+ bytes = ::Yeptris.read_input(bytes).to_s
226
+ report = ::FFI::MemoryPointer.new(:pointer)
227
+ st = FFI.yeptris_validate(bytes, bytes.bytesize, schema_id(schema), report)
228
+ consume_report(st, report, "validate")
229
+ end
230
+
231
+ # JSON Schema validation: `schema` is a compiled
232
+ # {Yeptris::Validation::Schema} (validate many) or schema bytes
233
+ # (one-shot). Returns the diagnostics array.
234
+ def validate_schema(bytes, schema)
235
+ require_engine!
236
+ case schema
237
+ when Schema then schema.validate(bytes)
238
+ when String then one_shot(bytes, schema)
239
+ else
240
+ raise ::ArgumentError,
241
+ "json_schema: must be a Yeptris::Validation::Schema or schema " \
242
+ "bytes (got #{schema.class})"
243
+ end
244
+ end
245
+
246
+ # The engine's whole-report drain shared by every face: maps the
247
+ # status, drains the diagnostics into plain values, frees the
248
+ # report — exactly once, on every path.
249
+ def consume_report(status, report, action)
250
+ ptr = report.read_pointer
251
+ diags = drain_report(ptr)
252
+ return diags if status == FFI::OK || status == FFI::SCHEMA_ERROR
253
+
254
+ raise ::ArgumentError, "#{action}: #{FFI.last_error_message}" if status == FFI::ERROR_ARG
255
+
256
+ raise ::Yeptris::ParseError, "#{action} failed: #{FFI.last_error_message}"
257
+ ensure
258
+ FFI.yeptris_validate_report_free(ptr) unless ptr.null?
259
+ end
260
+
261
+ # The schema symbol -> YeptrisSchema id (resolve.h's enum) for
262
+ # the VALIDATION family walks: only the YAML 1.2 families are
263
+ # validation families — the engine rejects :compat_11 with
264
+ # "schema 1 is not a validation family", surfaced here as the
265
+ # same ArgumentError the other bad symbols take.
266
+ def schema_id(name)
267
+ case name
268
+ when :core_12 then FFI::SCHEMA_12_CORE
269
+ when :json_12 then FFI::SCHEMA_12_JSON
270
+ when :compat_11
271
+ raise ::ArgumentError,
272
+ "schema: :compat_11 is not a validation family — the walk " \
273
+ "covers the YAML 1.2 families (:core_12, :json_12)"
274
+ else
275
+ raise ::ArgumentError,
276
+ "schema: must be :core_12, :compat_11, or :json_12 (got #{name.inspect})"
277
+ end
278
+ end
279
+
280
+ private
281
+
282
+ def one_shot(bytes, schema_bytes)
283
+ bytes = ::Yeptris.read_input(bytes).to_s
284
+ schema_bytes = schema_bytes.to_s
285
+ report = ::FFI::MemoryPointer.new(:pointer)
286
+ st = FFI.yeptris_json_schema_validate(schema_bytes, schema_bytes.bytesize,
287
+ bytes, bytes.bytesize, report)
288
+ consume_report(st, report, "json schema validate")
289
+ end
290
+
291
+ # drain before free: the diagnostics are read into plain Ruby
292
+ # while the report is still alive. The texts re-tag UTF-8 (the
293
+ # binding's string discipline — #135: engine materializations
294
+ # ride UTF-8, not the raw BINARY read).
295
+ def drain_report(report)
296
+ return [] if report.null?
297
+
298
+ FFI.yeptris_validate_report_count(report).times.map do |i|
299
+ d = FFI::CDiagnostic.new(FFI.yeptris_validate_report_at(report, i))
300
+ Diagnostic.new(
301
+ code: d[:code],
302
+ path: d.pointer.get_string(FFI::CDiagnostic.offset_of(:path))
303
+ .force_encoding(::Encoding::UTF_8),
304
+ detail: d.pointer.get_string(FFI::CDiagnostic.offset_of(:detail))
305
+ .force_encoding(::Encoding::UTF_8),
306
+ line: d[:line], col: d[:col], offset: d[:offset]
307
+ )
308
+ end
309
+ end
310
+
311
+ def require_engine!
312
+ return if FFI::VALIDATION
313
+
314
+ raise ::Yeptris::Error,
315
+ "yeptris: the validation face needs libyeptris 0.6.59+; the " \
316
+ "loaded library predates it. Update the engine library."
317
+ end
318
+ end
319
+ end
320
+ end
data/lib/yeptris/yaml.rb CHANGED
@@ -22,17 +22,64 @@ module Yeptris
22
22
  # schema: :compat_11 selects Psych/libyaml implicit typing
23
23
  # (yes/no, 0o/octal, sexagesimal); :core_12 (default) is YAML 1.2.
24
24
  #
25
+ # The validation keywords (#882) check BEFORE loading and raise
26
+ # Yeptris::Validation::Error carrying the diagnostics on invalid
27
+ # input: `validate:` rides the engine's schema-family walk (true
28
+ # rides :core_12 — the 1.2 families are the validation families;
29
+ # or an explicit :core_12 / :json_12), `json_schema:` validates
30
+ # against JSON Schema draft 2020-12 (schema bytes or a compiled
31
+ # Yeptris::Validation::Schema).
32
+ #
25
33
  # This surface keeps the Psych contract for EVERY input — including
26
34
  # JSON-shaped ones (`{"a": [1,]}` is legal flow YAML; `"1e3"` is a
27
35
  # Psych String). Strict RFC 8259 semantics live on Yeptris::JSON
28
36
  # (TODO.restructure/31): defaults follow proof, not benchmarks.
29
- def load(yaml, schema: :compat_11)
37
+ def load(yaml, schema: :compat_11, validate: nil, json_schema: nil)
30
38
  yaml = Yeptris.read_input(yaml)
31
39
  yaml = yaml.to_s
40
+ _assert_valid(yaml, validate, json_schema) if validate || json_schema
32
41
  docs = _drain_all(yaml, schema)
33
42
  docs.empty? ? nil : docs.first
34
43
  end
35
44
 
45
+ # The validation face (#882): every diagnostic the engine reports,
46
+ # as plain Yeptris::Validation::Diagnostic values (empty array =
47
+ # valid). Default is the engine's core-family walk; `json_schema:`
48
+ # switches to JSON Schema draft 2020-12 (bytes or a compiled
49
+ # Yeptris::Validation::Schema), `validate:` picks the schema family
50
+ # explicitly (:core_12 / :json_12). Every document of the stream is
51
+ # validated; paths are root-relative per document.
52
+ def validate(yaml, validate: true, json_schema: nil)
53
+ yaml = Yeptris.read_input(yaml)
54
+ _validation_call(yaml.to_s, validate, json_schema)
55
+ end
56
+
57
+ # The boolean posture of validate: true when the engine reports no
58
+ # diagnostics.
59
+ def valid?(yaml, validate: true, json_schema: nil)
60
+ validate(yaml, validate: validate, json_schema: json_schema).empty?
61
+ end
62
+
63
+ # The load-side gate: raises Yeptris::Validation::Error carrying
64
+ # the diagnostics when the input is invalid; silent otherwise.
65
+ def _assert_valid(yaml, validate, json_schema)
66
+ diags = _validation_call(yaml, validate, json_schema)
67
+ raise Validation::Error, diags unless diags.empty?
68
+ end
69
+
70
+ def _validation_call(yaml, validate, json_schema)
71
+ if json_schema
72
+ Validation.validate_schema(yaml, json_schema)
73
+ else
74
+ # `validate: true` rides :core_12 — the family walk has no
75
+ # :compat_11 face (the engine rejects it; resolve schemas are
76
+ # not validation families).
77
+ Validation.validate_family(yaml, schema: validate == true ? :core_12 : validate)
78
+ end
79
+ end
80
+ private_class_method :_validation_call
81
+ private_class_method :_assert_valid
82
+
36
83
  # Psych-semantics safe_load on the native surface (issue #69 —
37
84
  # the entry frameworks actually want): plain data only; a leaf
38
85
  # whose class is not permitted raises Yeptris::Psych::
@@ -60,13 +107,23 @@ module Yeptris
60
107
  #
61
108
  # With a block, every element — documents and, under :collect,
62
109
  # error markers — is yielded in order; the array is returned.
63
- def load_stream(yaml, schema: :compat_11, errors: :raise, &block)
110
+ #
111
+ # The validation keywords (#882) run the whole stream BEFORE
112
+ # loading (the same shapes as load: `validate:` family walk /
113
+ # `json_schema:` JSON Schema 2020-12) and raise
114
+ # Yeptris::Validation::Error on any diagnostics. The engine's
115
+ # report is flat across the stream — diagnostics carry no
116
+ # per-document index (streaming validation is future work, #885) —
117
+ # so the verdict is whole-stream by construction.
118
+ def load_stream(yaml, schema: :compat_11, errors: :raise, validate: nil,
119
+ json_schema: nil, &block)
64
120
  unless %i[raise collect].include?(errors)
65
121
  raise ArgumentError, "errors: must be :raise or :collect (got #{errors.inspect})"
66
122
  end
67
123
 
68
124
  yaml = Yeptris.read_input(yaml)
69
125
  yaml = yaml.to_s
126
+ _assert_valid(yaml, validate, json_schema) if validate || json_schema
70
127
  docs =
71
128
  begin
72
129
  docs = _drain_all(yaml, schema)
@@ -176,8 +233,11 @@ module Yeptris
176
233
  # (TODO.impl/11 phase 3). Strings are emitted plain only when the
177
234
  # resolver round-trips them as strings — everything else takes a
178
235
  # quoted style, so dump(load(x)) == x for the scalar types.
179
- def dump(obj, canonical: false, header: false)
180
- BulkBuilder.dump(obj, canonical: canonical, header: header)
236
+ # width: the emitter's best_width (the engine wraps flow
237
+ # collections after a value when the line exceeds it; 0/nil rides
238
+ # the engine default of 80 — emit.h 13B).
239
+ def dump(obj, canonical: false, header: false, width: nil)
240
+ BulkBuilder.dump(obj, canonical: canonical, header: header, width: width)
181
241
  end
182
242
 
183
243
 
@@ -228,7 +288,7 @@ module Yeptris
228
288
  Yeptris::FFI::BUILD_SEQ => SEQ_ENTRY,
229
289
  Yeptris::FFI::BUILD_END => END_ENTRY }.freeze
230
290
 
231
- def dump(obj, canonical: false, header: false)
291
+ def dump(obj, canonical: false, header: false, width: nil)
232
292
  parts = []
233
293
  blob = String.new(encoding: Encoding::BINARY)
234
294
  off = [0]
@@ -243,7 +303,8 @@ module Yeptris
243
303
  bblob = ::FFI::MemoryPointer.from_string(blob)
244
304
  rc = doc.build_entries(buf, parts.length, bblob, blob.bytesize)
245
305
  raise DumpError, "document_build failed: #{rc}" unless rc == FFI::OK
246
- doc.serialize(canonical: canonical, explicit_doc_start: header)
306
+ doc.serialize(canonical: canonical, best_width: width || 0,
307
+ explicit_doc_start: header)
247
308
  ensure
248
309
  doc&.free
249
310
  end
data/lib/yeptris.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  module Yeptris
4
4
  # The gem's version lives in the parent namespace's file — the last
5
5
  # internal require (yeptris/version) retired with it.
6
- VERSION = "0.6.57.1".freeze
6
+ VERSION = "0.6.59.1".freeze
7
7
 
8
8
  # Informational notices are OPT-IN (yeptris-ruby#217): consumers
9
9
  # embed yeptris inside CLIs that assert clean stderr, and a
@@ -54,6 +54,7 @@ module Yeptris
54
54
  autoload :ValueML, "yeptris/valueml"
55
55
  autoload :Psych, "yeptris/psych"
56
56
  autoload :Schema, "yeptris/schema"
57
+ autoload :Validation, "yeptris/validation"
57
58
  autoload :CBOR, "yeptris/cbor"
58
59
  end
59
60