yeptris 0.6.58.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 +4 -4
- data/lib/yeptris/document.rb +15 -0
- data/lib/yeptris/ffi.rb +93 -0
- data/lib/yeptris/psych/visitors.rb +16 -6
- data/lib/yeptris/psych.rb +11 -5
- data/lib/yeptris/validation.rb +320 -0
- data/lib/yeptris/yaml.rb +67 -6
- data/lib/yeptris.rb +2 -1
- data/vendor/libyeptris/CMakeLists.txt +1 -1
- data/vendor/libyeptris/src/CMakeLists.txt +1 -0
- data/vendor/libyeptris/src/include/yeptris/validate.h +50 -7
- data/vendor/libyeptris/src/yeptris/parse.c +8 -3
- data/vendor/libyeptris/src/yeptris/validate/compile.c +270 -0
- data/vendor/libyeptris/src/yeptris/validate/internal.h +34 -5
- data/vendor/libyeptris/src/yeptris/validate/jsonschema.c +420 -50
- data/vendor/libyeptris/src/yeptris/validate/regexp.c +207 -45
- data/vendor/libyeptris/src/yeptris/validate/validate.c +55 -94
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5f9da8cd9b7db4c514ad70beefc657e82c11464804ef6c6e9a451462c7d4831c
|
|
4
|
+
data.tar.gz: 6f94ccb3a3499f4c71dc24c00231309e6da9cef893d8bdcea60da2d7b2dce456
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7af7add33be58c2f972314fa3a235c83b455036ae74db7367a000a11ae13851d723955b770b63192aa68f54883f4fe5a15465c12547599afdb09d7c9231f5aef
|
|
7
|
+
data.tar.gz: feabebeaeb648b9810921337daf3466b85047b85f01072cad817277662bdfa5250fd40d4597b810abc769a5bf2ca1d5f299b1778130f5778bc85f2e2b2883964
|
data/lib/yeptris/document.rb
CHANGED
|
@@ -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
|
-
#
|
|
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(
|
|
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,
|
|
61
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
180
|
-
|
|
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,
|
|
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.
|
|
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
|
|