yang_json 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: fa4c5688935f52cb5b7f4c19617c931412a8292e80ae74172f9038f8ce2b51a1
4
+ data.tar.gz: c37fa39548ca49e450eba98cb78d3d15c84cf64de1387cb35294327ab92e266b
5
+ SHA512:
6
+ metadata.gz: 86e6486fcc19d66b68bc9b65e904f748557e24e5c19dde37fc3c00c0c8c2c27205570b0db297666feac0dda9cf055926bbd30c590a2268c631a8c05a520ee2f7
7
+ data.tar.gz: 78bf06358c8d754cf5614e1ff69f1eca28787c325453390fb576851e65d191d462f99ab1eee926a96f5fe44b6aee028aafe787748b892cff4279c0514bcb5a6f
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 - 2026-10-12
4
+
5
+ Initial release.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Yudai Takada
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # yang_json
2
+
3
+ Read, validate and write [RFC 7951](https://www.rfc-editor.org/rfc/rfc7951) YANG JSON in Ruby, including [RFC 7952](https://www.rfc-editor.org/rfc/rfc7952) metadata. Supply a schema as a Ruby Hash or JSON document. No external schema compiler, YANG parser, or subprocess is required.
4
+
5
+ ## Installation
6
+
7
+ ```ruby
8
+ gem "yang_json", "~> 0.1.0"
9
+ ```
10
+
11
+ Ruby 3.3 or later, JRuby and TruffleRuby are tested. Runtime dependencies are `json` (2.19.4–3.x), `bigdecimal` and `base64`; no other gem in this family is required. JSON 2.x and 3.x both reject duplicate object members explicitly.
12
+
13
+ ## Schema and data
14
+
15
+ Define the nodes and their types directly, or load the same structure with `Schema.from_json`. See [schema format](docs/jtox-format.md).
16
+
17
+ ```ruby
18
+ require "yang_json"
19
+
20
+ schema = YangJSON::Schema.new({
21
+ "modules" => { "orchard" => ["o", "urn:example:orchard"] },
22
+ "tree" => { "orchard:grove" => ["container", { "count" => ["leaf", "uint64"] }] },
23
+ "annotations" => {}
24
+ })
25
+ data = YangJSON.decode('{"orchard:grove":{"count":"123"}}', schema)
26
+ data["orchard:grove"]["orchard:count"] # => 123
27
+ YangJSON.encode(data, schema) # => '{"orchard:grove":{"count":"123"}}'
28
+ schema.lookup("/orchard:grove/count").type # => "uint64"
29
+ YangJSON.validate('{"orchard:grove":{"count":123}}', schema).first.pointer
30
+ # => "/orchard:grove/count"
31
+ ```
32
+
33
+ `decode` returns a mutable `YangJSON::Document`, a Hash with `warnings` and `union_members`. All modeled Ruby keys are **module-qualified**, including descendants. `encode` requires that form and writes the minimally qualified names required by RFC 7951. Opaque `anydata`/`anyxml` content retains its wire names. The schema and its nodes are immutable and shareable; conversions do not mutate inputs. `YangJson` is an alias for `YangJSON`.
34
+
35
+ `YangJSON.parse(json, schema: schema)` is an alternate spelling of `decode`. `schema.lookup` accepts a slash-separated path or an array of names; descendants may use simple or qualified names. List nodes expose their qualified `keys`.
36
+
37
+ ## Values
38
+
39
+ | YANG type | Ruby value | JSON representation |
40
+ |---|---|---|
41
+ | int8/int16/int32, uint8/uint16/uint32 | Integer, within the type's range | Number |
42
+ | int64, uint64 | Integer, within the type's range | Decimal string |
43
+ | decimal64 | BigDecimal, exact scaled 64-bit value | Decimal string |
44
+ | string, enumeration | Unicode String | String |
45
+ | boolean | true / false | true / false |
46
+ | bits | Array of unique names | Whitespace-separated string |
47
+ | binary | ASCII-8BIT String (`bytes.b`) | Standard padded base64 |
48
+ | identityref | Module-qualified String | Local name or qualified name |
49
+ | empty | true | `[null]` |
50
+ | union | Native value of a matching member | Matching member's JSON type |
51
+ | instance-identifier | String | Absolute path with module-name qualification |
52
+ | leafref | Native value of the resolved target type | Target type's representation |
53
+ | anydata | Hash of opaque JSON values | Object subject to RFC 7951 §5.5 |
54
+ | anyxml | Opaque JSON value | I-JSON value |
55
+
56
+ Decimal strings normalize on encoding (`"12.30"` becomes `"12.3"`, negative zero becomes `"0.0"`). Integer signs/leading zeros, bit whitespace and local identity prefixes also normalize. Binary decoding requires canonical padding and zero pad bits. Integer JSON tokens containing fractions/exponents are not accepted as small integer values.
57
+
58
+ Unions try compatible members in schema order, using the JSON type during decoding. Documents remember the selected member at each path, so JSON `1` and `"1"` remain distinct even when both become Ruby Integers. Plain Hash inputs use the first compatible member. Moving values to different paths, reordering arrays, or discarding the Document can discard this original selection; select unambiguous schema members when that distinction matters.
59
+
60
+ Metadata uses `@` inside container/list-entry/anydata objects, or `@module:leaf` beside modeled native leaves, anyxml and leaf-lists. Annotation names always stay qualified, and values use their declared types. Leaf-list metadata arrays preserve null holes and may omit trailing nulls. Registered annotations within opaque anydata are converted; its data values otherwise remain in wire form.
61
+
62
+ ## Validation and limits
63
+
64
+ ```ruby
65
+ data = YangJSON.decode(json, schema, strict: false)
66
+ data.warnings.each { |violation| warn "#{violation.pointer}: #{violation.message}" }
67
+ ```
68
+
69
+ Strict decoding raises `ValidationError`; lenient decoding retains unknown or invalid values and attaches diagnostics. It does not make invalid configuration safe. Encoding is always strict. Unknown schema fields cannot be serialized until the schema is extended or the fields are removed. Modeled keys normalize in lenient results, so malformed namespace aliases cannot always be preserved independently.
70
+
71
+ `ValidationError#violations` contains `Violation` objects with `pointer`, `message` and `rule`; `ValidationError#pointer` identifies the first failure. Pointers address the input document. `validate` returns violations, including parse failures. `ParseError` covers malformed JSON/schemas and resource limits; `ConversionError` covers generation errors. All inherit from `YangJSON::Error`. Unknown options, nonpositive limits and invalid schema arguments raise `ArgumentError`.
72
+
73
+ | Option | Default | Applies to |
74
+ |---|---:|---|
75
+ | `max_bytes` | 16 MiB | Input/output JSON; aggregate native string/key bytes |
76
+ | `max_depth` | 32 | JSON/native/schema nesting |
77
+ | `max_nodes` | 100,000 | Total values visited |
78
+ | `max_items` | 10,000 | Members of an object or items of an array |
79
+ | `max_binary_bytes` | 1 MiB | Decoded/encoded binary payload |
80
+
81
+ Pass overrides to `decode`, `encode`, `parse`, `validate` or `Schema.from_json`. Integer/decimal strings have an additional 128-character bound. Invalid Unicode, noncharacters, nonfinite numbers and duplicate members always fail, including lenient mode. Opaque integer values are restricted to ±(2^53−1); represent larger numbers as strings. Large objects, cyclic Ruby data and deep nesting are rejected before recursive conversion can exhaust normal limits.
82
+
83
+ ## Scope
84
+
85
+ | Supported | Outside the 0.1 scope |
86
+ |---|---|
87
+ | RFC 7951 names, all built-in value encodings, containers/lists, key presence and duplicate key tuples | Parsing YANG/YIN; XML, NETCONF and RESTCONF transport APIs |
88
+ | Pre-resolved built-in types, leafrefs, union members, and augmented nodes supplied by the caller | Compiling YANG typedefs, uses, augment, choice, RPC, action or notification definitions |
89
+ | Exact decimal64, 64-bit integers, union member selection and identity namespace rules | Complete YANG semantic/datastore validation |
90
+ | RFC 7952 typed annotations on each node category | Interpreting annotation application semantics |
91
+ | Structural schema-less anydata validation and opaque anyxml | Inferring or resolving schemas inside opaque content |
92
+
93
+ **The schema format omits semantic constraints.** This gem cannot validate enumeration membership, identity derivation, allowed bit names/positions, derived range/length/pattern restrictions, mandatory/default/config properties, choice exclusivity, min/max-elements, unique statements, must/when expressions, leafref existence or instance-identifier existence. It does not add defaults or reorder system-ordered collections. Repeated leaf-list values are accepted because state leaf-lists may contain duplicates and the schema omits `config`. Instance-identifier syntax and namespace changes are checked, but node existence and predicate suitability are not resolved. See [schema format notes](docs/jtox-format.md).
94
+
95
+ ## Development and conformance
96
+
97
+ ```sh
98
+ bundle install
99
+ bundle exec rake test
100
+ COVERAGE=1 bundle exec rake test
101
+ bundle exec rbs -r bigdecimal -I sig validate
102
+ ```
103
+
104
+ The Ruby suite covers all 20 JSON/path examples in RFC 7951/7952, including the deliberately invalid union value and complete Appendix A. Test schemas are declared locally. RFC excerpts are fetched with pinned SHA-256 hashes into ignored `tmp/rfc/`, retaining source notices and extraction corrections. These documents and examples are test inputs, never dependencies or packaged files. No external implementation is downloaded or executed by the tests. See [conformance notes](docs/conformance.md).
105
+
106
+ `bundle exec ruby script/benchmark.rb` measures a bounded large record and limit rejection. CI tests maintained CRuby versions, head, JRuby and TruffleRuby on Linux/macOS, and both JSON major versions. Coverage must reach 90%.
107
+
108
+ ## License and security
109
+
110
+ Released under the [MIT License](LICENSE.txt). Added runtime and development dependencies must offer MIT terms; Ruby, its standard libraries and their system dependencies are the explicit exception. See [implementation and dependency policy](docs/implementation.md). Report vulnerabilities through the private channel in [SECURITY.md](SECURITY.md).
data/SECURITY.md ADDED
@@ -0,0 +1,12 @@
1
+ # Security
2
+
3
+ Report vulnerabilities privately through [GitHub security advisories](https://github.com/ydah/yang_json/security/advisories/new).
4
+ Include the Ruby/json versions, a minimal schema and input, and the observed impact. Do not include credentials or private configuration data.
5
+
6
+ The 0.1.x series receives security fixes. This gem converts RFC 7951 data using a caller-supplied schema; it does not authenticate input, execute XPath, resolve remote modules, or implement complete YANG datastore validation. Use a full YANG validator when semantic constraints are required.
7
+
8
+ JSON member duplication, invalid UTF-8/noncharacters, nonfinite numbers, malformed metadata, and structure/type violations are rejected. Encoding always validates. Lenient decoding preserves unsupported values with warnings and must not be treated as accepted configuration. Review `warnings` before using such data.
9
+
10
+ Byte, nesting, node, collection and binary limits apply before or during conversion. Schema loading is also bounded. Raising these limits increases resource exposure. Pass trusted schemas and limits; protect application-level request sizes and concurrency separately. `anyxml` and schema-less `anydata` are opaque data, never executable XML or code.
11
+
12
+ Conformance documents and their notices are downloaded into ignored `tmp/` only. External implementations are not downloaded or executed by the tests. They are excluded from Git and the gem package. Dependencies retain their own licenses.
data/data/encoding.yml ADDED
@@ -0,0 +1,21 @@
1
+ # Original encoding reference; normative rules are cited, not reproduced.
2
+ names: {rfc: '7951', section: '4', ruby: 'qualified String keys', json: 'minimal module qualification'}
3
+ container: {rfc: '7951', section: '5.2', ruby: Hash, json: object}
4
+ leaf-list: {rfc: '7951', section: '5.3', ruby: Array, json: array}
5
+ list: {rfc: '7951', section: '5.4', ruby: 'Array of Hash', json: 'array of objects'}
6
+ anydata: {rfc: '7951', section: '5.5', ruby: 'Hash with opaque content', json: object}
7
+ anyxml: {rfc: '7951', section: '5.6', ruby: 'JSON-compatible value', json: 'I-JSON value'}
8
+ int8_int16_int32_uint8_uint16_uint32: {rfc: '7951', section: '6.1', ruby: Integer, json: number}
9
+ int64_uint64: {rfc: '7951', section: '6.1', ruby: Integer, json: string}
10
+ decimal64: {rfc: '7951', section: '6.1', ruby: BigDecimal, json: string}
11
+ string: {rfc: '7951', section: '6.2', ruby: String, json: string}
12
+ boolean: {rfc: '7951', section: '6.3', ruby: boolean, json: boolean}
13
+ enumeration: {rfc: '7951', section: '6.4', ruby: String, json: string}
14
+ bits: {rfc: '7951', section: '6.5', ruby: 'Array of String', json: string}
15
+ binary: {rfc: '7951', section: '6.6', ruby: 'ASCII-8BIT String', json: 'base64 string'}
16
+ leafref: {rfc: '7951', section: '6.7', ruby: 'resolved referent type', json: 'resolved referent type'}
17
+ identityref: {rfc: '7951', section: '6.8', erratum: 7020, ruby: 'module-qualified String', json: string}
18
+ empty: {rfc: '7951', section: '6.9', ruby: 'true', json: '[null]'}
19
+ union: {rfc: '7951', section: '6.10', ruby: 'native member type', json: 'selected member type'}
20
+ instance-identifier: {rfc: '7951', section: '6.11', ruby: String, json: string}
21
+ metadata: {rfc: '7952', section: '5.2', ruby: 'typed annotations', json: 'annotation object or positional array'}
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "base64"
5
+ require "bigdecimal"
6
+
7
+ module YangJSON
8
+ class Error < StandardError; end
9
+ class ParseError < Error; end
10
+ Violation = Data.define(:pointer, :message, :rule)
11
+
12
+ class ValidationError < Error
13
+ attr_reader :violations, :pointer
14
+
15
+ def initialize(violations)
16
+ @violations = violations.freeze
17
+ @pointer = violations.first&.pointer || ""
18
+ super(violations.first&.message || "Invalid YANG data")
19
+ end
20
+ end
21
+
22
+ class ConversionError < Error; end
23
+
24
+ class Document < Hash
25
+ attr_reader :warnings, :union_members
26
+
27
+ def initialize(values, warnings:, union_members: {})
28
+ super()
29
+ update(values)
30
+ @warnings = warnings.freeze
31
+ @union_members = union_members.freeze
32
+ end
33
+ end
34
+
35
+ module Core
36
+ IDENTIFIER = /\A[A-Za-z_][A-Za-z0-9_.-]*\z/
37
+ NAME = /\A(?:[A-Za-z_][A-Za-z0-9_.-]*:)?[A-Za-z_][A-Za-z0-9_.-]*\z/
38
+ DEFAULTS = { max_bytes: 16 * 1024 * 1024, max_depth: 32, max_nodes: 100_000, max_items: 10_000, max_binary_bytes: 1024 * 1024 }.freeze
39
+
40
+ module_function
41
+
42
+ def limits(options)
43
+ unknown = options.keys - DEFAULTS.keys
44
+ raise ArgumentError, "Unknown limit option" unless unknown.empty?
45
+
46
+ result = DEFAULTS.merge(options)
47
+ raise ArgumentError, "Limits must be positive integers" unless result.values.all? { |value| value.is_a?(Integer) && value.positive? }
48
+
49
+ result.freeze
50
+ end
51
+
52
+ def pointer(parent, name)
53
+ "#{parent}/#{name.to_s.gsub("~", "~0").gsub("/", "~1")}"
54
+ end
55
+
56
+ def parse(source, limits)
57
+ raise ParseError, "JSON source must be a string" unless source.is_a?(String)
58
+ raise ParseError, "Input byte limit exceeded" if source.bytesize > limits[:max_bytes]
59
+
60
+ source = source.dup.force_encoding(Encoding::UTF_8)
61
+ raise ParseError, "Invalid UTF-8" unless source.valid_encoding?
62
+
63
+ result = JSON.parse(source, allow_duplicate_key: false, max_nesting: limits[:max_depth], allow_nan: false)
64
+ copy(result, limits)
65
+ rescue JSON::ParserError, EncodingError, ArgumentError
66
+ raise ParseError, "Invalid JSON", cause: nil
67
+ end
68
+
69
+ def string(value)
70
+ return false unless value.is_a?(String) && value.valid_encoding? && (value.encoding == Encoding::UTF_8 || value.ascii_only?)
71
+
72
+ value.each_codepoint.none? { |point| (0xFDD0..0xFDEF).cover?(point) || (point & 0xFFFF) >= 0xFFFE }
73
+ end
74
+
75
+ def copy(value, limits = DEFAULTS, native: false, depth: 0, budget: [0, 0])
76
+ budget[0] += 1
77
+ raise ParseError, "Node count limit exceeded" if budget[0] > limits[:max_nodes]
78
+ raise ParseError, "Nesting limit exceeded" if depth > limits[:max_depth]
79
+
80
+ case value
81
+ when Hash
82
+ raise ParseError, "Object member limit exceeded" if value.length > limits[:max_items]
83
+ raise ParseError, "Invalid JSON member name" unless value.keys.all? { |name| string(name) }
84
+
85
+ budget[1] += value.keys.sum(&:bytesize)
86
+ raise ParseError, "Aggregate string byte limit exceeded" if budget[1] > limits[:max_bytes]
87
+
88
+ value.to_h { |name, item| [name.dup, copy(item, limits, native: native, depth: depth + 1, budget: budget)] }
89
+ when Array
90
+ raise ParseError, "Array item limit exceeded" if value.length > limits[:max_items]
91
+
92
+ value.map { |item| copy(item, limits, native: native, depth: depth + 1, budget: budget) }
93
+ when String
94
+ raise ParseError, "Invalid Unicode string" unless (native && value.encoding == Encoding::ASCII_8BIT) || string(value)
95
+
96
+ budget[1] += value.bytesize
97
+ raise ParseError, "String byte limit exceeded" if budget[1] > limits[:max_bytes]
98
+
99
+ value.dup
100
+ when Float
101
+ raise ParseError, "Non-finite number" unless value.finite?
102
+
103
+ value
104
+ when BigDecimal
105
+ raise ParseError, "Unexpected decimal object" unless native && value.finite?
106
+
107
+ value
108
+ when Integer, TrueClass, FalseClass, NilClass then value
109
+ else raise ParseError, "Unsupported Ruby value"
110
+ end
111
+ end
112
+
113
+ def freeze_tree(value)
114
+ case value
115
+ when Hash then value.each do |key, item|
116
+ key.freeze
117
+ freeze_tree(item)
118
+ end
119
+ when Array then value.each { |item| freeze_tree(item) }
120
+ end
121
+ value.freeze
122
+ end
123
+
124
+ def precise(value)
125
+ case value
126
+ when Hash then value.transform_values { |item| precise(item) }
127
+ when Array then value.map { |item| precise(item) }
128
+ when Float
129
+ return value unless RUBY_ENGINE == "truffleruby"
130
+
131
+ number = format("%.17g", value)
132
+ number += ".0" unless number.match?(/[.eE]/)
133
+ JSON::Fragment.new(number)
134
+ else value
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YangJSON
4
+ class Node
5
+ attr_reader :name, :module_name, :kind, :type, :children, :keys
6
+
7
+ def initialize(name:, kind:, type: nil, children: {}, keys: [])
8
+ @name = name.freeze
9
+ @module_name = name.split(":", 2).first.freeze
10
+ @kind = kind.freeze
11
+ @type = Core.freeze_tree(type)
12
+ @children = children.freeze
13
+ @keys = Core.freeze_tree(keys)
14
+ freeze
15
+ end
16
+ end
17
+
18
+ class Schema
19
+ attr_reader :modules, :tree, :annotations
20
+
21
+ # @param source [String] a schema in the documented JSON format
22
+ # @return [Schema] immutable schema
23
+ def self.from_json(source, **options)
24
+ limits = Core.limits(options)
25
+ new(Core.parse(source, limits), limits: limits)
26
+ end
27
+
28
+ class << self
29
+ alias from_jtox from_json
30
+ end
31
+
32
+ def initialize(data, limits: Core::DEFAULTS)
33
+ raise ParseError, "jtox schema must contain modules, tree, and annotations" unless data.is_a?(Hash) && %w[modules tree annotations].all? do |key|
34
+ data[key].is_a?(Hash)
35
+ end
36
+
37
+ data = Core.copy(data, limits)
38
+ @modules = data.fetch("modules")
39
+ @modules.each do |name, definition|
40
+ unless Core::IDENTIFIER.match?(name) && definition.is_a?(Array) && definition.length == 2 && definition.all?(String) && Core::IDENTIFIER.match?(definition[0])
41
+ raise ParseError, "Invalid jtox module definition"
42
+ end
43
+ end
44
+ @modules = Core.freeze_tree(@modules)
45
+ @tree = build_nodes(data.fetch("tree"), nil)
46
+ @annotations = data.fetch("annotations").to_h do |name, type|
47
+ qualified(name, nil)
48
+ check_type(type)
49
+ [name, Core.freeze_tree(type)]
50
+ end.freeze
51
+ freeze
52
+ end
53
+
54
+ # @param path [String, Array<String>] qualified root and simple/qualified descendants
55
+ # @return [Node, nil] schema node, or nil when absent
56
+ def lookup(path)
57
+ parts = path.is_a?(String) ? path.delete_prefix("/").split("/") : path
58
+ return nil unless parts.is_a?(Array)
59
+
60
+ nodes = tree
61
+ parent = nil
62
+ node = nil
63
+ parts.each do |part|
64
+ name = part.include?(":") ? part : "#{parent}:#{part}"
65
+ node = nodes[name]
66
+ return nil unless node
67
+
68
+ parent = node.module_name
69
+ nodes = node.children
70
+ end
71
+ node
72
+ end
73
+
74
+ private
75
+
76
+ def qualified(name, parent)
77
+ raise ParseError, "Invalid jtox node name" unless name.is_a?(String) && Core::NAME.match?(name)
78
+
79
+ full = name.include?(":") ? name : "#{parent}:#{name}"
80
+ raise ParseError, "Unknown or absent jtox module" unless modules.key?(full.split(":", 2).first)
81
+
82
+ full
83
+ end
84
+
85
+ def check_type(type)
86
+ if type.is_a?(String)
87
+ raise ParseError, "Unsupported or unresolved jtox type" unless Types::PRIMITIVES.include?(type)
88
+ elsif type.is_a?(Array) && type.length == 2
89
+ if type[0] == "decimal64"
90
+ raise ParseError, "Invalid fraction-digits" unless type[1].is_a?(Integer) && (1..18).cover?(type[1])
91
+ elsif type[0] == "union" && type[1].is_a?(Array) && !type[1].empty?
92
+ type[1].each { |member| check_type(member) }
93
+ else
94
+ raise ParseError, "Invalid jtox compound type"
95
+ end
96
+ else
97
+ raise ParseError, "Invalid jtox type"
98
+ end
99
+ end
100
+
101
+ def build_nodes(tree, parent)
102
+ raise ParseError, "Invalid jtox tree" unless tree.is_a?(Hash)
103
+
104
+ tree.each_with_object({}) do |(name, definition), output|
105
+ full = qualified(name, parent)
106
+ raise ParseError, "Duplicate normalized jtox node" if output.key?(full)
107
+ raise ParseError, "Invalid jtox node" unless definition.is_a?(Array)
108
+
109
+ kind = definition[0]
110
+ case kind
111
+ when "container", "list"
112
+ expected = kind == "list" ? 3 : 2
113
+ raise ParseError, "Invalid jtox branch" unless definition.length == expected
114
+
115
+ children = build_nodes(definition[1], full.split(":", 2).first)
116
+ keys = kind == "list" ? definition[2] : []
117
+ raise ParseError, "Invalid jtox list keys" unless keys.is_a?(Array) && keys.all? { |key| key.is_a?(Array) && key.length == 2 && key.all?(String) }
118
+
119
+ keys = keys.map { |key| key.join(":") }
120
+ raise ParseError, "List key must reference a child leaf" unless keys.uniq == keys && keys.all? { |key| children[key]&.kind == "leaf" }
121
+
122
+ output[full] = Node.new(name: full, kind: kind, children: children, keys: keys)
123
+ when "leaf", "leaf-list"
124
+ raise ParseError, "Invalid jtox leaf" unless definition.length == 2
125
+
126
+ check_type(definition[1])
127
+ output[full] = Node.new(name: full, kind: kind, type: definition[1])
128
+ when "anydata", "anyxml"
129
+ raise ParseError, "Invalid jtox opaque node" unless definition.length == 1
130
+
131
+ output[full] = Node.new(name: full, kind: kind)
132
+ else raise ParseError, "Unsupported jtox node kind"
133
+ end
134
+ end.freeze
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,245 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YangJSON
4
+ class Transformer
5
+ attr_reader :violations, :selections
6
+
7
+ def initialize(schema, mode:, limits:, selections: {})
8
+ raise ArgumentError, "Expected a YangJSON::Schema" unless schema.is_a?(Schema)
9
+
10
+ @schema = schema
11
+ @mode = mode
12
+ @limits = limits
13
+ @violations = []
14
+ @selections = selections.dup
15
+ end
16
+
17
+ def transform(value)
18
+ object(value, @schema.tree, nil, "", "", metadata_allowed: false)
19
+ end
20
+
21
+ private
22
+
23
+ def invalid(pointer, message, rule)
24
+ @violations << Violation.new(pointer: pointer.freeze, message: message.freeze, rule: rule.freeze)
25
+ end
26
+
27
+ def object(value, nodes, parent, pointer, location, metadata_allowed: true)
28
+ unless value.is_a?(Hash)
29
+ invalid(pointer, "Expected an object", "structure")
30
+ return value
31
+ end
32
+ output = {}
33
+ normalized = {}
34
+ value.each do |name, item|
35
+ next if name.start_with?("@")
36
+
37
+ path = Core.pointer(pointer, name)
38
+ unless Core::NAME.match?(name)
39
+ invalid(path, "Invalid YANG member name", "name")
40
+ output[name] = item
41
+ next
42
+ end
43
+ full = if name.include?(":")
44
+ name
45
+ else
46
+ (parent ? "#{parent}:#{name}" : name)
47
+ end
48
+ node = nodes[full]
49
+ unless node
50
+ invalid(path, "Unknown schema node", "schema")
51
+ output[full] = item
52
+ normalized[name] = [full, nil]
53
+ next
54
+ end
55
+ expected = @mode == :encode ? full : wire_name(node, parent)
56
+ invalid(path, "Invalid module qualification", "namespace") unless name == expected
57
+ target = @mode == :decode ? full : wire_name(node, parent)
58
+ if output.key?(target)
59
+ invalid(path, "Duplicate normalized member", "duplicate")
60
+ next
61
+ end
62
+ normalized[name] = [target, node]
63
+ output[target] = node_value(item, node, path, Core.pointer(location, full))
64
+ end
65
+ # Resolve data members first: metadata is allowed to precede its target.
66
+ value.each do |name, item| # rubocop:disable Style/CombinableLoops
67
+ next unless name.start_with?("@")
68
+
69
+ path = Core.pointer(pointer, name)
70
+ if name == "@"
71
+ invalid(path, "Metadata has no enclosing data node", "metadata") unless metadata_allowed
72
+ output[name] = annotations(item, path, Core.pointer(location, "@"))
73
+ next
74
+ end
75
+ target, node = normalized[name.delete_prefix("@")]
76
+ unless node
77
+ invalid(path, "Metadata must accompany an existing schema node", "metadata")
78
+ output[name] = item
79
+ next
80
+ end
81
+ if node.kind == "leaf-list"
82
+ if !item.is_a?(Array) || !value[name.delete_prefix("@")].is_a?(Array) || item.length > value[name.delete_prefix("@")].length
83
+ invalid(path, "Leaf-list metadata must align with its values", "metadata")
84
+ output["@#{target}"] = item
85
+ else
86
+ output["@#{target}"] = item.each_with_index.map do |entry, index|
87
+ entry.nil? ? nil : annotations(entry, Core.pointer(path, index), Core.pointer(location, "@#{node.name}/#{index}"))
88
+ end
89
+ end
90
+ elsif %w[leaf anyxml].include?(node.kind)
91
+ output["@#{target}"] = annotations(item, path, Core.pointer(location, "@#{node.name}"))
92
+ else
93
+ invalid(path, "Object node metadata belongs inside its @ member", "metadata")
94
+ output["@#{target}"] = item
95
+ end
96
+ end
97
+ output
98
+ end
99
+
100
+ def node_value(value, node, pointer, location)
101
+ case node.kind
102
+ when "container" then object(value, node.children, node.module_name, pointer, location)
103
+ when "list"
104
+ unless value.is_a?(Array)
105
+ invalid(pointer, "List must be an array", "structure")
106
+ return value
107
+ end
108
+ entries = value.each_with_index.map do |entry, index|
109
+ object(entry, node.children, node.module_name, Core.pointer(pointer, index), Core.pointer(location, index))
110
+ end
111
+ seen = {}
112
+ entries.each_with_index do |entry, index|
113
+ next unless entry.is_a?(Hash)
114
+
115
+ keys = node.keys.map { |name| @mode == :decode ? name : wire_name(node.children.fetch(name), node.module_name) }
116
+ missing = keys.reject { |key| entry.key?(key) }
117
+ missing.each do |key|
118
+ child = node.children.values.find { |candidate| candidate.name == key || wire_name(candidate, node.module_name) == key }
119
+ input_key = @mode == :decode ? wire_name(child, node.module_name) : child.name
120
+ invalid(Core.pointer(Core.pointer(pointer, index), input_key), "Required list key is missing", "key")
121
+ end
122
+ next unless missing.empty? && !keys.empty?
123
+
124
+ tuple = keys.map { |key| entry[key] }
125
+ invalid(Core.pointer(pointer, index), "Duplicate list key tuple", "unique") if seen[tuple]
126
+ seen[tuple] = true
127
+ end
128
+ entries
129
+ when "leaf" then typed(value, node.type, node.module_name, pointer, location)
130
+ when "leaf-list"
131
+ unless value.is_a?(Array)
132
+ invalid(pointer, "Leaf-list must be an array", "structure")
133
+ return value
134
+ end
135
+ # jtox omits config; YANG 1.1 permits repeated values for state leaf-lists.
136
+ value.each_with_index.map do |entry, index|
137
+ typed(entry, node.type, node.module_name, Core.pointer(pointer, index), Core.pointer(location, index))
138
+ end
139
+
140
+ when "anydata"
141
+ unless value.is_a?(Hash)
142
+ invalid(pointer, "anydata must be an object", "structure")
143
+ return value
144
+ end
145
+ anydata_object(value, pointer, location)
146
+ when "anyxml"
147
+ opaque(value, pointer, anydata: false)
148
+ value
149
+ end
150
+ end
151
+
152
+ def typed(value, type, module_name, pointer, location)
153
+ Types.convert(value, type, mode: @mode, module_name: module_name, schema: @schema, limits: @limits,
154
+ pointer: location, selections: @selections)
155
+ rescue ConversionError => e
156
+ invalid(pointer, e.message, "type")
157
+ value
158
+ end
159
+
160
+ def annotations(value, pointer, location)
161
+ unless value.is_a?(Hash)
162
+ invalid(pointer, "Metadata object must be an object", "metadata")
163
+ return value
164
+ end
165
+ value.to_h do |name, item|
166
+ path = Core.pointer(pointer, name)
167
+ type = @schema.annotations[name]
168
+ if !Core::NAME.match?(name) || !name.include?(":") || !type
169
+ invalid(path, "Unknown or unqualified metadata annotation", "metadata")
170
+ [name, item]
171
+ else
172
+ [name, typed(item, type, name.split(":", 2).first, path, Core.pointer(location, name))]
173
+ end
174
+ end
175
+ end
176
+
177
+ def wire_name(node, parent)
178
+ node.module_name == parent ? node.name.split(":", 2).last : node.name
179
+ end
180
+
181
+ # Names and values inside schema-less anydata remain in wire form. Only
182
+ # registered annotations can be assigned native types without a content schema.
183
+ def anydata_object(value, pointer, location)
184
+ value.to_h do |name, item|
185
+ path = Core.pointer(pointer, name)
186
+ position = Core.pointer(location, name)
187
+ if name == "@"
188
+ [name, annotations(item, path, position)]
189
+ elsif name.start_with?("@")
190
+ sibling = value[name.delete_prefix("@")]
191
+ invalid(path, "Metadata must accompany a scalar or leaf-list", "metadata") unless value.key?(name.delete_prefix("@")) && !sibling.is_a?(Hash)
192
+ if sibling.is_a?(Array) && sibling != [nil]
193
+ if item.is_a?(Array) && item.length <= sibling.length && sibling.none?(Hash)
194
+ [name, item.each_with_index.map { |entry, i| entry.nil? ? nil : annotations(entry, Core.pointer(path, i), Core.pointer(position, i)) }]
195
+ else
196
+ invalid(path, "Leaf-list metadata must align with its values", "metadata")
197
+ [name, item]
198
+ end
199
+ else
200
+ [name, annotations(item, path, position)]
201
+ end
202
+ else
203
+ invalid(path, "Invalid anydata member name", "anydata") unless Core::NAME.match?(name)
204
+ if item.is_a?(Hash)
205
+ [name, anydata_object(item, path, position)]
206
+ elsif item.is_a?(Array) && item.all?(Hash)
207
+ [name, item.each_with_index.map { |entry, i| anydata_object(entry, Core.pointer(path, i), Core.pointer(position, i)) }]
208
+ else
209
+ opaque(item, path, anydata: true)
210
+ [name, item]
211
+ end
212
+ end
213
+ end
214
+ end
215
+
216
+ def opaque(value, pointer, anydata:)
217
+ case value
218
+ when Hash
219
+ value.each do |name, item|
220
+ invalid(Core.pointer(pointer, name), "Invalid anydata member name", "anydata") if anydata && !Core::NAME.match?(name)
221
+ opaque(item, Core.pointer(pointer, name), anydata: anydata)
222
+ end
223
+ when Array
224
+ if anydata
225
+ return if value == [nil]
226
+
227
+ objects = value.all?(Hash)
228
+ scalars = value.all? { |entry| entry == [nil] || !(entry.is_a?(Hash) || entry.is_a?(Array) || entry.nil?) }
229
+ unless objects || (scalars && value.uniq.length == value.length)
230
+ invalid(pointer, "anydata array must contain objects or unique scalar values", "anydata")
231
+ end
232
+ end
233
+ value.each_with_index { |item, index| opaque(item, Core.pointer(pointer, index), anydata: anydata) }
234
+ when NilClass
235
+ invalid(pointer, "anydata null is only valid as [null]", "anydata") if anydata
236
+ when Integer
237
+ invalid(pointer, "Opaque JSON integer exceeds the interoperable range", "i-json") if value.abs > ((2**53) - 1)
238
+ when BigDecimal
239
+ invalid(pointer, "Opaque JSON cannot contain native decimal values", "i-json")
240
+ when String
241
+ invalid(pointer, "Opaque JSON requires Unicode strings", "i-json") unless Core.string(value)
242
+ end
243
+ end
244
+ end
245
+ end
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YangJSON
4
+ module Types
5
+ PRIMITIVES = %w[int8 int16 int32 int64 uint8 uint16 uint32 uint64 string enumeration boolean bits binary identityref empty
6
+ instance-identifier].each(&:freeze).freeze
7
+ INTEGER_TYPES = %w[int8 int16 int32 int64 uint8 uint16 uint32 uint64].each(&:freeze).freeze
8
+
9
+ module_function
10
+
11
+ def convert(value, type, mode:, module_name:, schema:, limits:, pointer:, selections:)
12
+ name, parameter = type.is_a?(Array) ? type : [type, nil]
13
+ if name == "union"
14
+ indices = (0...parameter.length).to_a
15
+ preferred = selections[pointer] if mode == :encode
16
+ indices = [preferred] + (indices - [preferred]) if preferred && indices.include?(preferred)
17
+ indices.each do |index|
18
+ result = convert(value, parameter[index], mode: mode, module_name: module_name, schema: schema, limits: limits,
19
+ pointer: "#{pointer}/union#{index}", selections: selections)
20
+ selections[pointer] = index if mode == :decode
21
+ return result
22
+ rescue ConversionError
23
+ next
24
+ end
25
+ raise ConversionError, "Value does not match any union member"
26
+ end
27
+ case name
28
+ when *INTEGER_TYPES then integer(value, name, mode)
29
+ when "decimal64" then decimal(value, parameter, mode)
30
+ when "string", "enumeration"
31
+ raise ConversionError, "Expected a Unicode string" unless Core.string(value)
32
+
33
+ value.dup
34
+ when "boolean"
35
+ raise ConversionError, "Expected a boolean" unless [true, false].include?(value)
36
+
37
+ value
38
+ when "empty"
39
+ expected = mode == :decode ? [nil] : true
40
+ raise ConversionError, "Invalid empty value" unless value == expected
41
+
42
+ mode == :decode ? true : [nil]
43
+ when "bits" then bits(value, mode)
44
+ when "binary" then binary(value, mode, limits[:max_binary_bytes])
45
+ when "identityref" then identity(value, mode, module_name, schema)
46
+ when "instance-identifier"
47
+ instance_identifier(value, schema)
48
+ value.dup
49
+ else raise ConversionError, "Unsupported data type"
50
+ end
51
+ end
52
+
53
+ def integer(value, type, mode)
54
+ bits = type.delete_prefix("u").delete_prefix("int").to_i
55
+ if mode == :decode && bits == 64
56
+ raise ConversionError, "64-bit integer must be a decimal string" unless value.is_a?(String) && value.match?(/\A[+-]?[0-9]+\z/)
57
+ raise ConversionError, "Integer representation is too long" if value.length > 128
58
+
59
+ number = Integer(value, 10)
60
+ else
61
+ raise ConversionError, "Expected an integer" unless value.is_a?(Integer)
62
+
63
+ number = value
64
+ end
65
+ range = type.start_with?("u") ? (0..((2**bits) - 1)) : (-(2**(bits - 1))..((2**(bits - 1)) - 1))
66
+ raise ConversionError, "Integer is out of range" unless range.cover?(number)
67
+
68
+ mode == :encode && bits == 64 ? number.to_s : number
69
+ end
70
+
71
+ def decimal(value, digits, mode)
72
+ if mode == :decode
73
+ unless value.is_a?(String) && value.length <= 128 && value.match?(/\A[+-]?[0-9]+(?:\.[0-9]+)?\z/)
74
+ raise ConversionError, "decimal64 must be a decimal string"
75
+ end
76
+
77
+ number = BigDecimal(value)
78
+ else
79
+ raise ConversionError, "decimal64 requires BigDecimal" unless value.is_a?(BigDecimal) && value.finite?
80
+
81
+ number = value
82
+ end
83
+ scaled = number * (10**digits)
84
+ raise ConversionError, "decimal64 precision or range exceeded" unless scaled.frac.zero? && scaled >= -(2**63) && scaled <= (2**63) - 1
85
+
86
+ if mode == :decode
87
+ number
88
+ else
89
+ (number.zero? ? "0.0" : number.to_s("F"))
90
+ end
91
+ end
92
+
93
+ def bits(value, mode)
94
+ if mode == :decode
95
+ raise ConversionError, "bits requires a string" unless value.is_a?(String)
96
+
97
+ names = value.split(/[ \t\r\n]+/).reject(&:empty?)
98
+ else
99
+ raise ConversionError, "bits requires an array" unless value.is_a?(Array)
100
+
101
+ names = value
102
+ end
103
+ unless names.all? { |name| name.is_a?(String) && Core::IDENTIFIER.match?(name) } && names.uniq == names
104
+ raise ConversionError, "Invalid or duplicate bit name"
105
+ end
106
+
107
+ mode == :decode ? names : names.join(" ")
108
+ end
109
+
110
+ def binary(value, mode, limit)
111
+ raise ConversionError, "binary requires a string" unless value.is_a?(String)
112
+
113
+ if mode == :decode
114
+ raise ConversionError, "Decoded binary limit exceeded" if value.bytesize > ((limit + 2) / 3) * 4
115
+
116
+ bytes = Base64.strict_decode64(value)
117
+ raise ConversionError, "Noncanonical base64 value" unless Base64.strict_encode64(bytes) == value
118
+ else
119
+ raise ConversionError, "binary requires an ASCII-8BIT string" unless value.encoding == Encoding::ASCII_8BIT
120
+
121
+ bytes = value
122
+ end
123
+ raise ConversionError, "Binary byte limit exceeded" if bytes.bytesize > limit
124
+
125
+ mode == :decode ? bytes : Base64.strict_encode64(bytes)
126
+ rescue ArgumentError
127
+ raise ConversionError, "Invalid base64 value", cause: nil
128
+ end
129
+
130
+ def identity(value, mode, parent, schema)
131
+ raise ConversionError, "Invalid identity name" unless value.is_a?(String) && Core::NAME.match?(value)
132
+ raise ConversionError, "Ruby identity must be module-qualified" if mode == :encode && !value.include?(":")
133
+
134
+ full = value.include?(":") ? value : "#{parent}:#{value}"
135
+ module_name, name = full.split(":", 2)
136
+ raise ConversionError, "Unknown identity module" unless schema.modules.key?(module_name)
137
+
138
+ if mode == :decode
139
+ full
140
+ else
141
+ (module_name == parent ? name : full)
142
+ end
143
+ end
144
+
145
+ def instance_identifier(value, schema)
146
+ raise ConversionError, "instance-identifier must be an absolute path" unless Core.string(value) && value.start_with?("/")
147
+
148
+ offset = 0
149
+ parent = nil
150
+ while offset < value.length
151
+ match = %r{\G/((?:[A-Za-z_][A-Za-z0-9_.-]*:)?[A-Za-z_][A-Za-z0-9_.-]*)}.match(value, offset)
152
+ raise ConversionError, "Invalid instance-identifier step" unless match
153
+
154
+ name = match[1]
155
+ module_name = name.include?(":") ? name.split(":", 2).first : parent
156
+ unless module_name && schema.modules.key?(module_name) && (name.include?(":") == (module_name != parent))
157
+ raise ConversionError, "Invalid instance-identifier namespace"
158
+ end
159
+
160
+ parent = module_name
161
+ offset = match.end(0)
162
+ predicate_kind = nil
163
+ while value[offset] == "["
164
+ predicate = /\G\[[ \t]*(?:(?:((?:[A-Za-z_][A-Za-z0-9_.-]*:)?[A-Za-z_][A-Za-z0-9_.-]*|\.)[ \t]*=[ \t]*(?:'[^']*'|"[^"]*"))|[1-9][0-9]*)[ \t]*\]/.match(
165
+ value, offset
166
+ )
167
+ raise ConversionError, "Invalid instance-identifier predicate" unless predicate
168
+
169
+ key = predicate[1]
170
+ kind = key && key != "." ? :key : :single
171
+ raise ConversionError, "Incompatible instance-identifier predicates" if predicate_kind && (predicate_kind != :key || kind != :key)
172
+
173
+ predicate_kind = kind
174
+ if key && key != "." && key.include?(":")
175
+ module_name = key.split(":", 2).first
176
+ raise ConversionError, "Invalid predicate namespace" if module_name == parent || !schema.modules.key?(module_name)
177
+ end
178
+ offset = predicate.end(0)
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YangJSON
4
+ VERSION = "0.1.0"
5
+ end
6
+ YangJson = YangJSON
data/lib/yang_json.rb ADDED
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "yang_json/version"
4
+ require_relative "yang_json/core"
5
+ require_relative "yang_json/types"
6
+ require_relative "yang_json/schema"
7
+ require_relative "yang_json/transformer"
8
+
9
+ module YangJSON
10
+ # Decode RFC 7951 JSON to module-qualified Ruby values.
11
+ # @param source [String] UTF-8 JSON text
12
+ # @param schema [Schema] immutable caller-defined schema
13
+ # @param strict [Boolean] whether semantic violations raise an exception
14
+ # @return [Document] converted data, diagnostics and union selections
15
+ # @raise [ParseError] malformed JSON or a resource limit violation
16
+ # @raise [ValidationError] a type or structure violation in strict mode
17
+ def self.decode(source, schema, strict: true, **options)
18
+ limits = Core.limits(options)
19
+ value = Core.parse(source, limits)
20
+ transformer = Transformer.new(schema, mode: :decode, limits: limits)
21
+ result = transformer.transform(value)
22
+ raise ValidationError, transformer.violations if strict && !transformer.violations.empty?
23
+ raise ValidationError, transformer.violations unless result.is_a?(Hash)
24
+
25
+ Document.new(result, warnings: transformer.violations, union_members: transformer.selections)
26
+ end
27
+
28
+ # Encode native values; all validation failures are errors.
29
+ # @param data [Hash] native values with module-qualified modeled keys
30
+ # @param schema [Schema] immutable caller-defined schema
31
+ # @return [String] RFC 7951 JSON
32
+ # @raise [ValidationError] data cannot be represented by the schema
33
+ def self.encode(data, schema, **options)
34
+ limits = Core.limits(options)
35
+ value = Core.copy(data, limits, native: true)
36
+ selections = data.is_a?(Document) ? data.union_members : {}
37
+ transformer = Transformer.new(schema, mode: :encode, limits: limits, selections: selections)
38
+ result = transformer.transform(value)
39
+ raise ValidationError, transformer.violations unless transformer.violations.empty?
40
+
41
+ result = Core.copy(result, limits)
42
+ encoded = JSON.generate(Core.precise(result), allow_nan: false, max_nesting: limits[:max_depth])
43
+ raise ConversionError, "Output byte limit exceeded" if encoded.bytesize > limits[:max_bytes]
44
+
45
+ encoded
46
+ rescue JSON::GeneratorError
47
+ raise ConversionError, "Invalid JSON output", cause: nil
48
+ end
49
+
50
+ # @return [Array<Violation>] all diagnostics, including parse failures
51
+ def self.validate(source, schema, **)
52
+ decode(source, schema, strict: false, **).warnings
53
+ rescue ParseError => e
54
+ [Violation.new(pointer: "", message: e.message, rule: "parse")]
55
+ rescue ValidationError => e
56
+ e.violations
57
+ end
58
+
59
+ def self.parse(source, schema:, **)
60
+ decode(source, schema, **)
61
+ end
62
+ end
data/sig/yang_json.rbs ADDED
@@ -0,0 +1,53 @@
1
+ module YangJSON
2
+ VERSION: String
3
+ type json = nil | bool | Integer | Float | String | Array[json] | Hash[String, json]
4
+ type value = json | BigDecimal | Array[value] | Hash[String, value]
5
+ type limits = { ?max_bytes: Integer, ?max_depth: Integer, ?max_nodes: Integer, ?max_items: Integer, ?max_binary_bytes: Integer }
6
+ type yang_type = String | Array[untyped]
7
+
8
+ class Error < StandardError
9
+ end
10
+ class ParseError < Error
11
+ end
12
+ class ConversionError < Error
13
+ end
14
+ class Violation < Data
15
+ attr_reader pointer: String
16
+ attr_reader message: String
17
+ attr_reader rule: String
18
+ def self.new: (pointer: String, message: String, rule: String) -> instance
19
+ end
20
+ class ValidationError < Error
21
+ attr_reader violations: Array[Violation]
22
+ attr_reader pointer: String
23
+ def initialize: (Array[Violation]) -> void
24
+ end
25
+ class Document < Hash[String, value]
26
+ attr_reader warnings: Array[Violation]
27
+ attr_reader union_members: Hash[String, Integer]
28
+ def initialize: (Hash[String, value], warnings: Array[Violation], ?union_members: Hash[String, Integer]) -> void
29
+ end
30
+ class Node
31
+ attr_reader name: String
32
+ attr_reader module_name: String
33
+ attr_reader kind: String
34
+ attr_reader type: yang_type?
35
+ attr_reader children: Hash[String, Node]
36
+ attr_reader keys: Array[String]
37
+ def initialize: (name: String, kind: String, ?type: yang_type?, ?children: Hash[String, Node], ?keys: Array[String]) -> void
38
+ end
39
+ class Schema
40
+ attr_reader modules: Hash[String, Array[String]]
41
+ attr_reader tree: Hash[String, Node]
42
+ attr_reader annotations: Hash[String, yang_type]
43
+ def self.from_json: (String, **Integer) -> instance
44
+ def self.from_jtox: (String, **Integer) -> instance
45
+ def initialize: (Hash[String, untyped], ?limits: limits) -> void
46
+ def lookup: (String | Array[String]) -> Node?
47
+ end
48
+ def self.decode: (String, Schema, ?strict: bool, **Integer) -> Document
49
+ def self.parse: (String, schema: Schema, ?strict: bool, **Integer) -> Document
50
+ def self.encode: (Hash[String, value], Schema, **Integer) -> String
51
+ def self.validate: (String, Schema, **Integer) -> Array[Violation]
52
+ end
53
+ module YangJson = YangJSON
metadata ADDED
@@ -0,0 +1,118 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: yang_json
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Yudai Takada
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: base64
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0.2'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '1'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '0.2'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '1'
32
+ - !ruby/object:Gem::Dependency
33
+ name: bigdecimal
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '3.1'
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: '5'
42
+ type: :runtime
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '3.1'
49
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: '5'
52
+ - !ruby/object:Gem::Dependency
53
+ name: json
54
+ requirement: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: 2.19.4
59
+ - - "<"
60
+ - !ruby/object:Gem::Version
61
+ version: '4'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 2.19.4
69
+ - - "<"
70
+ - !ruby/object:Gem::Version
71
+ version: '4'
72
+ description: Schema-aware YANG JSON value conversion, namespace normalization, structural
73
+ validation, and RFC 7952 annotations.
74
+ email:
75
+ - t.yudai92@gmail.com
76
+ executables: []
77
+ extensions: []
78
+ extra_rdoc_files: []
79
+ files:
80
+ - CHANGELOG.md
81
+ - LICENSE.txt
82
+ - README.md
83
+ - SECURITY.md
84
+ - data/encoding.yml
85
+ - lib/yang_json.rb
86
+ - lib/yang_json/core.rb
87
+ - lib/yang_json/schema.rb
88
+ - lib/yang_json/transformer.rb
89
+ - lib/yang_json/types.rb
90
+ - lib/yang_json/version.rb
91
+ - sig/yang_json.rbs
92
+ homepage: https://github.com/ydah/yang_json
93
+ licenses:
94
+ - MIT
95
+ metadata:
96
+ allowed_push_host: https://rubygems.org
97
+ homepage_uri: https://github.com/ydah/yang_json
98
+ source_code_uri: https://github.com/ydah/yang_json/tree/main
99
+ changelog_uri: https://github.com/ydah/yang_json/blob/main/CHANGELOG.md
100
+ rubygems_mfa_required: 'true'
101
+ rdoc_options: []
102
+ require_paths:
103
+ - lib
104
+ required_ruby_version: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: '3.3'
109
+ required_rubygems_version: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: '0'
114
+ requirements: []
115
+ rubygems_version: 4.0.16
116
+ specification_version: 4
117
+ summary: Read and write RFC 7951 YANG JSON using caller-defined schemas
118
+ test_files: []