yeptris 0.6.58.1-arm-linux-eabihf → 0.6.60.1-arm-linux-eabihf

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/ext/yeptris_native/version.h +2 -2
  3. data/lib/yeptris/3.4/native.so +0 -0
  4. data/lib/yeptris/document.rb +15 -0
  5. data/lib/yeptris/ffi.rb +112 -0
  6. data/lib/yeptris/psych/visitors.rb +16 -6
  7. data/lib/yeptris/psych.rb +11 -5
  8. data/lib/yeptris/validation.rb +320 -0
  9. data/lib/yeptris/yaml.rb +128 -6
  10. data/lib/yeptris.rb +2 -1
  11. data/libyeptris.so +0 -0
  12. data/vendor/libyeptris/CMakeLists.txt +1 -1
  13. data/vendor/libyeptris/src/CMakeLists.txt +8 -0
  14. data/vendor/libyeptris/src/include/yeptris/emit.h +30 -2
  15. data/vendor/libyeptris/src/include/yeptris/parse.h +52 -0
  16. data/vendor/libyeptris/src/include/yeptris/resolve.h +8 -0
  17. data/vendor/libyeptris/src/include/yeptris/validate.h +259 -15
  18. data/vendor/libyeptris/src/include/yeptris/values.h +8 -2
  19. data/vendor/libyeptris/src/yeptris/cbor/cbor.h +10 -0
  20. data/vendor/libyeptris/src/yeptris/cbor/decode.c +5 -7
  21. data/vendor/libyeptris/src/yeptris/cbor/encode.c +9 -9
  22. data/vendor/libyeptris/src/yeptris/cbor/from_marshal.c +1 -1
  23. data/vendor/libyeptris/src/yeptris/common/error.h +2 -0
  24. data/vendor/libyeptris/src/yeptris/doc.h +20 -0
  25. data/vendor/libyeptris/src/yeptris/emit/emit.c +85 -13
  26. data/vendor/libyeptris/src/yeptris/emit/writer.c +158 -642
  27. data/vendor/libyeptris/src/yeptris/emit/writer.h +15 -1
  28. data/vendor/libyeptris/src/yeptris/events/push.c +28 -3
  29. data/vendor/libyeptris/src/yeptris/events/values.c +24 -0
  30. data/vendor/libyeptris/src/yeptris/marshal/to_json.c +650 -0
  31. data/vendor/libyeptris/src/yeptris/parse/engine.c +121 -40
  32. data/vendor/libyeptris/src/yeptris/parse/stream_iter.c +326 -0
  33. data/vendor/libyeptris/src/yeptris/parse.c +181 -46
  34. data/vendor/libyeptris/src/yeptris/resolve/compat11.c +1 -1
  35. data/vendor/libyeptris/src/yeptris/resolve/core12.c +11 -3
  36. data/vendor/libyeptris/src/yeptris/resolve/full12.c +201 -0
  37. data/vendor/libyeptris/src/yeptris/resolve/json12.c +12 -6
  38. data/vendor/libyeptris/src/yeptris/resolve/resolver.h +20 -0
  39. data/vendor/libyeptris/src/yeptris/validate/compile.c +306 -0
  40. data/vendor/libyeptris/src/yeptris/validate/internal.h +255 -10
  41. data/vendor/libyeptris/src/yeptris/validate/jsonschema.c +1025 -125
  42. data/vendor/libyeptris/src/yeptris/validate/output.c +246 -0
  43. data/vendor/libyeptris/src/yeptris/validate/regexp.c +791 -163
  44. data/vendor/libyeptris/src/yeptris/validate/registry.c +118 -0
  45. data/vendor/libyeptris/src/yeptris/validate/resources.c +601 -0
  46. data/vendor/libyeptris/src/yeptris/validate/stream.c +1155 -0
  47. data/vendor/libyeptris/src/yeptris/validate/validate.c +330 -226
  48. metadata +10 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 23d3c8dc0b4600de5a60826f71cf3ffb2d7517c8a6e0c8f588aee724320782e3
4
- data.tar.gz: '067915418bf97cccf16cc015e4b78959d45270ff97c8d0372b988b9b3bab1b7c'
3
+ metadata.gz: 0e078deaa97a27d09dc305c57e5d879ec727b506c0edfd6c1ef27a3fa7bb8d98
4
+ data.tar.gz: 9d3a325cba29a0c5c51f33be9c99ccca3ce4febdb46dd4595d2e7d3dbb73988e
5
5
  SHA512:
6
- metadata.gz: 47511ed558cbf416de324e79679163e1dabcaecdbe009317c240015c6d063ddb3d212b215af60f8181d0e08c1d863f94ff4b5531362edc95c07f80bb1059f7d0
7
- data.tar.gz: afc2c53b583f6637902bdeb8e8d506309be3beb412af4d7f664a462332360ec50cb9340bea3909601776ab72a5b6e8aeebfd1ffe1bb22568cc13ccb145e0be96
6
+ metadata.gz: fa97a7e1b0332bcd8106b1c244d5a1b11641ff28c5be0fba465a84095cfc120a87e469a23e32acd7e19c6f1dbddabb2ce0c6b7eb372e3848a0866140d0771670
7
+ data.tar.gz: a54e2f72f58ef07db3ef50755aa03bfbc41381770f252387bc331e7c71ead5818bb8b352197960336f69056ebe04e25a3dc272ee43032645facddd7658239a30
@@ -11,8 +11,8 @@
11
11
 
12
12
  #define YEPTRIS_VERSION_MAJOR 0
13
13
  #define YEPTRIS_VERSION_MINOR 6
14
- #define YEPTRIS_VERSION_PATCH 58
15
- #define YEPTRIS_VERSION_STRING "0.6.58"
14
+ #define YEPTRIS_VERSION_PATCH 60
15
+ #define YEPTRIS_VERSION_STRING "0.6.60"
16
16
 
17
17
  #ifdef __cplusplus
18
18
  extern "C" {
Binary file
@@ -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
@@ -56,6 +56,7 @@ module Yeptris
56
56
 
57
57
 
58
58
  typedef :pointer, :yeptris_document
59
+ typedef :pointer, :yeptris_stream_iter
59
60
  typedef :pointer, :yeptris_node
60
61
  typedef :pointer, :yeptris_status_out
61
62
  typedef :int, :yeptris_status
@@ -87,6 +88,24 @@ module Yeptris
87
88
  %i[pointer size_t pointer yeptris_status_out], :yeptris_document
88
89
  attach_function :yeptris_parse_json, %i[pointer size_t yeptris_status_out], :yeptris_document
89
90
 
91
+ # The tolerant per-document stream iterator (engine #921): frames
92
+ # documents scanner-accurately, reports failures per document (the
93
+ # failed run's line/col ARE that document's own), resyncs to the
94
+ # next line-start marker. On an engine older than the iterator the
95
+ # attach stubs above raise the stale-engine message at USE
96
+ # (each_document's caller sees it directly — no separate gate
97
+ # constant, the @missing law covers it).
98
+ attach_function :yeptris_stream_iter_new, %i[pointer size_t pointer], :yeptris_stream_iter
99
+ attach_function :yeptris_stream_iter_next,
100
+ %i[yeptris_stream_iter pointer pointer], :yeptris_status
101
+ attach_function :yeptris_stream_iter_status,
102
+ %i[yeptris_stream_iter pointer pointer], :yeptris_status
103
+ attach_function :yeptris_stream_iter_free, [:yeptris_stream_iter], :void
104
+ # the capability probe (evaluated AFTER the attaches above — the
105
+ # CBOR.available? pattern): specs and hosts gate on it; at USE a
106
+ # stale engine still raises the stub's message
107
+ STREAM_ITER = !(@missing ||= []).include?(:yeptris_stream_iter_new)
108
+
90
109
  # The JSON tape (TODO.restructure/85; the JSON surface's load
91
110
  # engine — issue #81): ONE call, three bulk columns, spans borrow
92
111
  # the caller's string (no arena copy, no second validating parse).
@@ -427,6 +446,9 @@ module Yeptris
427
446
 
428
447
  SCHEMA_12_CORE = 0
429
448
  SCHEMA_11_COMPAT = 1
449
+ # resolve.h's third kind (spec 10.2): plain scalars must match a
450
+ # JSON production; the validate face's schema-family walk rides it.
451
+ SCHEMA_12_JSON = 2
430
452
 
431
453
  OK = 0
432
454
  ERROR_PARSE = 1
@@ -437,6 +459,96 @@ module Yeptris
437
459
  ERROR_ARG = 6
438
460
  ERROR_UNSUPPORTED = 7
439
461
  ERROR_INTERNAL = 8
462
+
463
+ # ---- the validation face (validate.h; #882, engine 0.6.59+) ----
464
+ # Whole reports: one call produces a report the caller drains
465
+ # (count/at) and frees once. SCHEMA_ERROR (9, defined above) means
466
+ # the walk completed and the instance is INVALID — drain the
467
+ # report. Absent on older libraries: the attachment helper records
468
+ # the miss and VALIDATION gates the Ruby face with the friendly
469
+ # error at use.
470
+
471
+ typedef :pointer, :yeptris_schema
472
+ typedef :pointer, :yeptris_validate_report
473
+
474
+ # yeptris_diagnostic (validate.h): ABI-frozen — four uint32 facts
475
+ # then two bounded NUL-terminated texts. 16 + 256 + 128 bytes.
476
+ class CDiagnostic < ::FFI::Struct
477
+ layout :code, :uint32,
478
+ :line, :uint32,
479
+ :col, :uint32,
480
+ :offset, :uint32,
481
+ :path, [:char, 256],
482
+ :detail, [:char, 128]
483
+ end
484
+
485
+ # The schema-family walk: parse + validate fused (tag consistency,
486
+ # canonical-form key uniqueness; the JSON family's string-key +
487
+ # JSON-production rules under SCHEMA_12_JSON).
488
+ attach_function :yeptris_validate, %i[pointer size_t int pointer], :int
489
+ # An already-parsed document (or stream) against the family walk.
490
+ attach_function :yeptris_validate_document, %i[yeptris_document int pointer], :int
491
+ # JSON Schema draft 2020-12 subset, one-shot (schema bytes parsed
492
+ # per call).
493
+ attach_function :yeptris_json_schema_validate,
494
+ %i[pointer size_t pointer size_t pointer], :int
495
+ # The compiled-schema economy (#885 item 3): parse once, validate
496
+ # many. NULL from compile = failure (yeptris_last_error carries why).
497
+ attach_function :yeptris_schema_compile, %i[pointer size_t], :yeptris_schema
498
+ attach_function :yeptris_schema_validate,
499
+ %i[yeptris_schema pointer size_t pointer], :int
500
+ attach_function :yeptris_schema_free, [:yeptris_schema], :void
501
+ attach_function :yeptris_validate_report_count,
502
+ [:yeptris_validate_report], :size_t
503
+ attach_function :yeptris_validate_report_at,
504
+ %i[yeptris_validate_report size_t], :pointer
505
+ attach_function :yeptris_validate_report_free,
506
+ [:yeptris_validate_report], :void
507
+
508
+ # The diag codes (validate.h's YeptrisDiagCode, appending-only;
509
+ # pinned values — the engine's test_abi pins the head).
510
+ DIAG_NONE = 0
511
+ DIAG_TAG_MISMATCH = 1
512
+ DIAG_KEY_NOT_STRING = 2
513
+ DIAG_NON_JSON_TYPE = 3
514
+ DIAG_DUPLICATE_KEY = 4
515
+ DIAG_TYPE = 5
516
+ DIAG_REQUIRED = 6
517
+ DIAG_ENUM = 7
518
+ DIAG_CONST = 8
519
+ DIAG_PATTERN = 9
520
+ DIAG_MINIMUM = 10
521
+ DIAG_MAXIMUM = 11
522
+ DIAG_EXCLUSIVE_MINIMUM = 12
523
+ DIAG_EXCLUSIVE_MAXIMUM = 13
524
+ DIAG_MIN_LENGTH = 14
525
+ DIAG_MAX_LENGTH = 15
526
+ DIAG_MIN_ITEMS = 16
527
+ DIAG_MAX_ITEMS = 17
528
+ DIAG_UNIQUE_ITEMS = 18
529
+ DIAG_ADDITIONAL_PROPERTY = 19
530
+ DIAG_ANY_OF = 20
531
+ DIAG_ONE_OF = 21
532
+ DIAG_NOT = 22
533
+ DIAG_REF = 23
534
+ DIAG_FALSE_SCHEMA = 24
535
+ DIAG_DEPTH = 25
536
+ DIAG_LIMIT = 26
537
+ DIAG_MULTIPLE_OF = 27
538
+ DIAG_MIN_PROPERTIES = 28
539
+ DIAG_MAX_PROPERTIES = 29
540
+ DIAG_CONTAINS = 30
541
+ DIAG_MIN_CONTAINS = 31
542
+ DIAG_MAX_CONTAINS = 32
543
+ DIAG_DEPENDENT_REQUIRED = 33
544
+
545
+ # The face's availability (engine 0.6.59+); the Validation module
546
+ # raises the friendly error at use when the loaded library predates
547
+ # the attachments (the stubs above would raise NotFoundError raw).
548
+ VALIDATION = !(@missing ||= []).include?(:yeptris_validate) &&
549
+ !(@missing ||= []).include?(:yeptris_schema_compile) &&
550
+ !(@missing ||= []).include?(:yeptris_validate_report_at)
551
+
440
552
  # The load-time essentials (the read path + the ownership
441
553
  # contract): absent on engines far older than the optional
442
554
  # 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