argot 1.0.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 +7 -0
- data/.rubocop.yml +0 -0
- data/.ruby-version +1 -0
- data/LICENSE.txt +22 -0
- data/README.md +107 -0
- data/Rakefile +10 -0
- data/lib/argot/annotation.rb +64 -0
- data/lib/argot/location.rb +12 -0
- data/lib/argot/schema.rb +386 -0
- data/lib/argot/tag.rb +76 -0
- data/lib/argot/tags/literal.rb +25 -0
- data/lib/argot/tags/oneof.rb +17 -0
- data/lib/argot/tags/optional.rb +14 -0
- data/lib/argot/tags/pattern.rb +26 -0
- data/lib/argot/tags/range.rb +47 -0
- data/lib/argot/tags/required.rb +14 -0
- data/lib/argot/tags/type.rb +45 -0
- data/lib/argot/validator.rb +291 -0
- data/lib/argot/version.rb +5 -0
- data/lib/argot.rb +17 -0
- data/sig/argot.rbs +4 -0
- metadata +81 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 96b176358ac20b03d7377f0555b06de9d0054cf464b686b9fda573381a0628c0
|
4
|
+
data.tar.gz: 8837551ecd8af3fe9306f37734e37a8a2ea1b8fc31c5b0568f4db7b410d53f70
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 65f000cb80437135c29ec679b712b6684f41b0a34e3cf8bab66511bd8a35ab1f4e12ef5a8b7b0b22be960f4d89180e528f69357aff8ecb3b1acb1ff0da72207b
|
7
|
+
data.tar.gz: 11ab730c7e7887a0c7d3dbc412b56f733a437594c82f1cc834a64a0c26da73e7fe7629a8fae79827b5a534945ea1d998a1140b397a8df72cb633d057d8586a26
|
data/.rubocop.yml
ADDED
File without changes
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
3.3.4
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2024 Charlton Trezevant
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person
|
4
|
+
obtaining a copy of this software and associated documentation
|
5
|
+
files (the "Software"), to deal in the Software without
|
6
|
+
restriction, including without limitation the rights to use,
|
7
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the
|
9
|
+
Software is furnished to do so, subject to the following
|
10
|
+
conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be
|
13
|
+
included in all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
17
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
19
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
20
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
21
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
# Argot
|
2
|
+
|
3
|
+
> **Argot** (/är′gō/)
|
4
|
+
> _noun_
|
5
|
+
>
|
6
|
+
> A characteristic language of a particular group (as among thieves).
|
7
|
+
|
8
|
+
Argot is a simple gem for quickly and flexibly building minimal, validatable YAML schemas. Its original inspiration came from [this blog post](https://notes.burke.libbey.me/yaml-schema/) by [@burke](https://github.com/burke).
|
9
|
+
|
10
|
+
Argot aims to be:
|
11
|
+
- Simple (i.e., non-bureuacratic)
|
12
|
+
- Flexible
|
13
|
+
- Easy to Understand and Extend
|
14
|
+
|
15
|
+
Argot aims to avoid:
|
16
|
+
- Arcane syntax
|
17
|
+
- Having many layers of abstraction between the schema you write and the document you validate
|
18
|
+
|
19
|
+
### Example Schema
|
20
|
+
|
21
|
+
```yaml
|
22
|
+
%TAG ! tag:argot.packfiles.io,2024:
|
23
|
+
---
|
24
|
+
|
25
|
+
# "version" is optional. When supplied, it must be the literal string "latest" or match the regular expression ^\d+.\d+.\d+$
|
26
|
+
# The !one tag describes a sequence of acceptable validation rules for a given key
|
27
|
+
!o version: !one
|
28
|
+
- !l "latest"
|
29
|
+
- !x '^\d+.\d+.\d+$'
|
30
|
+
|
31
|
+
# "farmer_name" is required, and its value must match the regular expression ^Farmer [a-zA-Z]+$
|
32
|
+
!r farmer_name: !x '^Farmer [a-zA-Z]+$'
|
33
|
+
|
34
|
+
# "farmer_level" must be an Integer type
|
35
|
+
!o farmer_level: !t Integer
|
36
|
+
|
37
|
+
# The !x tag can also be used to describe validations that are applied
|
38
|
+
# to any keys in a mapping whose name matches a regular expression
|
39
|
+
!o meats:
|
40
|
+
!x 'beef_from_.*':
|
41
|
+
!r grade: !one
|
42
|
+
- !l "A"
|
43
|
+
- !l "B"
|
44
|
+
- !l "C"
|
45
|
+
- !l "F"
|
46
|
+
|
47
|
+
# When a parent key is marked as optional, validation rules will be applied to its children only
|
48
|
+
# when that parent key is present in the document. In this case, the absence of the required
|
49
|
+
# pasture_uuid key will only fail validation if its parent goat_zone key is present.
|
50
|
+
!o farm:
|
51
|
+
!o goat_zone:
|
52
|
+
!r pasture_uuid: !x '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
|
53
|
+
!o goats_count: !t Integer
|
54
|
+
```
|
55
|
+
|
56
|
+
## Installation
|
57
|
+
|
58
|
+
Install the gem and add to the application's Gemfile by executing:
|
59
|
+
|
60
|
+
$ bundle add argot
|
61
|
+
|
62
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
63
|
+
|
64
|
+
$ gem install argot
|
65
|
+
|
66
|
+
## Usage
|
67
|
+
|
68
|
+
First, register an Argot schema:
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
test_schema = File.read("./test/fixtures/schemas/01.yml")
|
72
|
+
Argot::Schema.register(:test_schema, test_schema)
|
73
|
+
```
|
74
|
+
|
75
|
+
Then, validate some YAML files:
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
rules = Argot::Schema.for(:test_schema)
|
79
|
+
|
80
|
+
passing_document = File.read("./test/fixtures/documents/01-pass.yml")
|
81
|
+
failing_document = File.read("./test/fixtures/documents/01-fail.yml")
|
82
|
+
|
83
|
+
|
84
|
+
validator = Argot::Validator.parse_and_validate(rules, failing_document)
|
85
|
+
validator.errors? # true
|
86
|
+
validator.safe_load # nil
|
87
|
+
|
88
|
+
validator = Argot::Validator.parse_and_validate(rules, passing_document)
|
89
|
+
validator.errors? # false
|
90
|
+
validator.safe_load # parsed document Hash
|
91
|
+
```
|
92
|
+
|
93
|
+
## Development
|
94
|
+
|
95
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
96
|
+
|
97
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
98
|
+
|
99
|
+
## Contributing
|
100
|
+
|
101
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/chtzvt/argot.
|
102
|
+
|
103
|
+
## License
|
104
|
+
|
105
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
106
|
+
|
107
|
+
Made with :heart: by [Packfiles :package:](https://packfiles.io)
|
data/Rakefile
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
module Argot
|
2
|
+
class Annotation
|
3
|
+
include Comparable
|
4
|
+
|
5
|
+
NOTICE = :notice
|
6
|
+
WARNING = :warning
|
7
|
+
FAILURE = :failure
|
8
|
+
|
9
|
+
ORDERED_LEVELS = {
|
10
|
+
NOTICE => 0,
|
11
|
+
WARNING => 1,
|
12
|
+
FAILURE => 2
|
13
|
+
}
|
14
|
+
|
15
|
+
attr_reader :start_line, :end_line, :start_column, :end_column, :level, :message, :title, :details
|
16
|
+
attr_accessor :path
|
17
|
+
|
18
|
+
def initialize(path: nil, location: nil, level: nil, message: nil, title: nil, details: nil)
|
19
|
+
@path = path || "input"
|
20
|
+
|
21
|
+
self.location = if location.is_a? Argot::Location
|
22
|
+
location
|
23
|
+
else
|
24
|
+
Argot::Location.new(**location)
|
25
|
+
end
|
26
|
+
|
27
|
+
@level = level || NOTICE
|
28
|
+
@message = message
|
29
|
+
@title = title || message
|
30
|
+
@details = details || message
|
31
|
+
end
|
32
|
+
|
33
|
+
def location=(location)
|
34
|
+
@start_line = location.start_line
|
35
|
+
@end_line = location.end_line
|
36
|
+
@start_column = location.start_column
|
37
|
+
@end_column = location.end_column
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_h
|
41
|
+
{
|
42
|
+
path: @path,
|
43
|
+
start_line: @start_line,
|
44
|
+
end_line: @end_line,
|
45
|
+
start_column: @start_column,
|
46
|
+
end_column: @end_column,
|
47
|
+
annotation_level: @level,
|
48
|
+
message: @message,
|
49
|
+
title: @title,
|
50
|
+
raw_details: @details
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_s
|
55
|
+
msg = "[#{@level.to_s.upcase}] At lines:#{@start_line}-#{@end_line} cols:#{@start_column}-#{@end_column} of #{@path} -- #{@title}: #{@message}"
|
56
|
+
msg += " (#{@details})" unless @details == @message
|
57
|
+
msg
|
58
|
+
end
|
59
|
+
|
60
|
+
def <=>(other)
|
61
|
+
ORDERED_LEVELS[level] <=> ORDERED_LEVELS[other.level]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Argot
|
2
|
+
class Location
|
3
|
+
attr_accessor :start_line, :end_line, :start_column, :end_column
|
4
|
+
|
5
|
+
def initialize(start_line: nil, end_line: nil, start_column: nil, end_column: nil)
|
6
|
+
@start_line = start_line || 1
|
7
|
+
@end_line = end_line || start_line || 1
|
8
|
+
@start_column = start_column || 1
|
9
|
+
@end_column = end_column || start_column || 1
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
data/lib/argot/schema.rb
ADDED
@@ -0,0 +1,386 @@
|
|
1
|
+
module Argot
|
2
|
+
module Schema
|
3
|
+
def self.register(id, schema_yaml)
|
4
|
+
@schemas ||= {}
|
5
|
+
@schemas[id.is_a?(Symbol) ? id : id.to_sym] = load(schema_yaml)
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.for(id)
|
9
|
+
@schemas[id]&.dup
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.load(schema_yaml)
|
13
|
+
schema = Schema::Parser.parse(schema_yaml)
|
14
|
+
ruleset = Schema::Ruleset.new
|
15
|
+
Schema::Compiler.compile(schema.tree, ruleset)
|
16
|
+
ruleset
|
17
|
+
end
|
18
|
+
|
19
|
+
class Ruleset
|
20
|
+
attr_reader :rules
|
21
|
+
|
22
|
+
def initialize
|
23
|
+
@rules = Hash.new { |hash, key| hash[key] = {key: [], value: []} }
|
24
|
+
end
|
25
|
+
|
26
|
+
def add(type, tag, path)
|
27
|
+
case type
|
28
|
+
when :key
|
29
|
+
@rules[path][:key] << tag
|
30
|
+
when :value
|
31
|
+
@rules[path][:value] << tag
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def permit_path?(path)
|
36
|
+
return false if path.empty? || path.nil?
|
37
|
+
|
38
|
+
@rules.each_key do |key_path|
|
39
|
+
return true if match_path?(key_path, path)
|
40
|
+
end
|
41
|
+
|
42
|
+
false
|
43
|
+
end
|
44
|
+
|
45
|
+
def permit_value?(path, value)
|
46
|
+
rule = lookup(path)
|
47
|
+
|
48
|
+
return false if rule.nil?
|
49
|
+
|
50
|
+
rule[:value].any? { |tag| tag.validate(value) }
|
51
|
+
end
|
52
|
+
|
53
|
+
def lookup(path)
|
54
|
+
@rules.each do |key_path, rule_set|
|
55
|
+
return rule_set if match_path?(key_path, path)
|
56
|
+
end
|
57
|
+
nil
|
58
|
+
end
|
59
|
+
|
60
|
+
def paths
|
61
|
+
@rules.keys
|
62
|
+
end
|
63
|
+
|
64
|
+
def subkeys(partial_path)
|
65
|
+
return @rules.keys if partial_path.empty?
|
66
|
+
|
67
|
+
@rules.keys.map do |key_path|
|
68
|
+
if match_subpath?(key_path, partial_path)
|
69
|
+
next_element = key_path[partial_path.size]
|
70
|
+
[next_element] if next_element
|
71
|
+
end
|
72
|
+
end.compact!.uniq
|
73
|
+
end
|
74
|
+
|
75
|
+
def required_path?(path)
|
76
|
+
parent_path = path.take((path.length >= 1) ? path.length - 1 : 0)
|
77
|
+
parent_rule_set = lookup(parent_path)
|
78
|
+
rule_set = lookup(path)
|
79
|
+
|
80
|
+
if parent_rule_set.nil? && rule_set.nil?
|
81
|
+
false
|
82
|
+
elsif parent_rule_set.nil? && rule_set[:key].any? { |tag| tag.is_a?(Argot::Tag::Required) }
|
83
|
+
true
|
84
|
+
elsif !parent_rule_set.nil? && parent_rule_set[:key].any? { |tag| tag.is_a?(Argot::Tag::Optional) } && !rule_set.nil? && rule_set[:key].any? { |tag| tag.is_a?(Argot::Tag::Required) }
|
85
|
+
true
|
86
|
+
else
|
87
|
+
false
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def match_path?(key_path, lookup_path)
|
92
|
+
return false unless key_path.size == lookup_path.size
|
93
|
+
|
94
|
+
key_path.zip(lookup_path).all? do |key_part, lookup_part|
|
95
|
+
case key_part
|
96
|
+
when String
|
97
|
+
key_part == lookup_part
|
98
|
+
when Regexp
|
99
|
+
if lookup_part.is_a?(String)
|
100
|
+
key_part.match?(lookup_part)
|
101
|
+
elsif lookup_part.is_a?(Regexp)
|
102
|
+
key_part == lookup_part
|
103
|
+
end
|
104
|
+
else
|
105
|
+
false
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def match_subpath?(key_path, partial_path)
|
111
|
+
return false unless key_path.size > partial_path.size
|
112
|
+
|
113
|
+
key_path.first(partial_path.size).zip(partial_path).all? do |key_part, partial_part|
|
114
|
+
case key_part
|
115
|
+
when String
|
116
|
+
key_part == partial_part
|
117
|
+
when Regexp
|
118
|
+
if partial_part.is_a?(String)
|
119
|
+
key_part.match?(partial_part)
|
120
|
+
elsif partial_part.is_a?(Regexp)
|
121
|
+
key_part == partial_part
|
122
|
+
end
|
123
|
+
else
|
124
|
+
false
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
class Compiler
|
131
|
+
def self.compile(node, ruleset)
|
132
|
+
return unless node.is_a? Argot::Schema::Node
|
133
|
+
|
134
|
+
return unless node.value.is_a?(Array)
|
135
|
+
|
136
|
+
node.value.each do |child|
|
137
|
+
next if child.is_a? Argot::Schema::Node
|
138
|
+
|
139
|
+
child_node_annotation = child.first
|
140
|
+
child_node = child.last
|
141
|
+
|
142
|
+
if child_node_annotation.tag_is_a?(Tag::Pattern)
|
143
|
+
ruleset.add(:key, Tag::Optional.new, child_node.path)
|
144
|
+
else
|
145
|
+
ruleset.add(:key, child_node_annotation.tag, child_node.path)
|
146
|
+
end
|
147
|
+
|
148
|
+
if child_node.tag?
|
149
|
+
if child_node.tag_is_a?(Argot::Tag::OneOf)
|
150
|
+
child_node.value.each do |oneof_opt|
|
151
|
+
if oneof_opt.tag?
|
152
|
+
ruleset.add(:value, oneof_opt.tag, oneof_opt.path)
|
153
|
+
else
|
154
|
+
compile(oneof_opt, ruleset)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
else
|
158
|
+
ruleset.add(:value, child_node.tag, child_node.path)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
compile(child_node, ruleset)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
class Node
|
168
|
+
attr_accessor :value, :path
|
169
|
+
attr_reader :tag, :location
|
170
|
+
|
171
|
+
def initialize(value: nil, tag: nil, location: nil, path: [])
|
172
|
+
@value = value
|
173
|
+
self.tag = tag
|
174
|
+
configure_tag!
|
175
|
+
self.location = location
|
176
|
+
@path = path
|
177
|
+
end
|
178
|
+
|
179
|
+
def tag?
|
180
|
+
!@tag.nil?
|
181
|
+
end
|
182
|
+
|
183
|
+
def tag_is_a?(klass)
|
184
|
+
@tag.is_a?(klass)
|
185
|
+
end
|
186
|
+
|
187
|
+
def configure_tag!
|
188
|
+
@tag.configure(@value) if tag?
|
189
|
+
end
|
190
|
+
|
191
|
+
def tag=(tag)
|
192
|
+
@tag = Argot::Tag.for(tag)
|
193
|
+
end
|
194
|
+
|
195
|
+
def location=(loc)
|
196
|
+
@location = Argot::Location.new(**loc)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
class Parser < ::Psych::Handler
|
201
|
+
attr_reader :tree, :locations, :current_path
|
202
|
+
|
203
|
+
class << self
|
204
|
+
def parse(schema_yaml)
|
205
|
+
handler = Argot::Schema::Parser.new
|
206
|
+
parser = Psych::Parser.new(handler)
|
207
|
+
parser.parse(schema_yaml)
|
208
|
+
handler
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def initialize
|
213
|
+
super
|
214
|
+
@context = []
|
215
|
+
@tree = nil
|
216
|
+
@current_path = []
|
217
|
+
@locations = {}
|
218
|
+
@scalar_scanner = build_safe_scalar_scanner
|
219
|
+
end
|
220
|
+
|
221
|
+
def build_safe_scalar_scanner
|
222
|
+
class_loader = Psych::ClassLoader::Restricted.new([], [])
|
223
|
+
Psych::ScalarScanner.new(class_loader)
|
224
|
+
end
|
225
|
+
|
226
|
+
def context_empty?
|
227
|
+
@context.empty?
|
228
|
+
end
|
229
|
+
|
230
|
+
def current_context
|
231
|
+
@context.last
|
232
|
+
end
|
233
|
+
|
234
|
+
def in_sequence?
|
235
|
+
current_context&.key?(:sequence)
|
236
|
+
end
|
237
|
+
|
238
|
+
def in_mapping?
|
239
|
+
current_context&.key?(:mapping)
|
240
|
+
end
|
241
|
+
|
242
|
+
def push_path(val)
|
243
|
+
@current_path.push(val)
|
244
|
+
end
|
245
|
+
|
246
|
+
def pop_path
|
247
|
+
@current_path.pop
|
248
|
+
end
|
249
|
+
|
250
|
+
def dup_path
|
251
|
+
@current_path.dup
|
252
|
+
end
|
253
|
+
|
254
|
+
def event_location(start_line, start_column, end_line, end_column)
|
255
|
+
@current_location = {
|
256
|
+
start_line: start_line + 1,
|
257
|
+
end_line: end_line + 1,
|
258
|
+
start_column: start_column + 1,
|
259
|
+
end_column: end_column + 1
|
260
|
+
}
|
261
|
+
end
|
262
|
+
|
263
|
+
def start_mapping(anchor, tag, implicit, style)
|
264
|
+
@context.push({mapping: [], tag: tag, key: nil})
|
265
|
+
end
|
266
|
+
|
267
|
+
def end_mapping
|
268
|
+
mapping = @context.pop
|
269
|
+
|
270
|
+
node = Node.new(
|
271
|
+
value: mapping[:mapping],
|
272
|
+
tag: mapping[:tag],
|
273
|
+
location: @current_location.dup,
|
274
|
+
path: dup_path
|
275
|
+
)
|
276
|
+
|
277
|
+
if context_empty?
|
278
|
+
@tree = node
|
279
|
+
return
|
280
|
+
end
|
281
|
+
|
282
|
+
if in_sequence?
|
283
|
+
current_context[:sequence] << node
|
284
|
+
elsif in_mapping?
|
285
|
+
if current_context[:key].nil?
|
286
|
+
raise Argot::Unprocessable, "Unexpected state in end_mapping"
|
287
|
+
else
|
288
|
+
current_context[:mapping] << [current_context[:key], node]
|
289
|
+
current_context[:key] = nil
|
290
|
+
pop_path
|
291
|
+
end
|
292
|
+
else
|
293
|
+
raise Argot::Unprocessable, "Unknown context in end_mapping"
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
def scalar(value, anchor, tag, plain, quoted, style)
|
298
|
+
parsed_value = parse_scalar(value)
|
299
|
+
|
300
|
+
node = Node.new(
|
301
|
+
value: parsed_value,
|
302
|
+
tag: tag,
|
303
|
+
location: @current_location.dup,
|
304
|
+
path: dup_path
|
305
|
+
)
|
306
|
+
|
307
|
+
if context_empty?
|
308
|
+
@tree = node
|
309
|
+
return
|
310
|
+
end
|
311
|
+
|
312
|
+
if in_sequence?
|
313
|
+
current_context[:sequence] << node
|
314
|
+
return
|
315
|
+
end
|
316
|
+
|
317
|
+
if in_mapping?
|
318
|
+
if current_context[:key].nil?
|
319
|
+
# We are processing a key
|
320
|
+
if node.tag_is_a?(Tag::Pattern)
|
321
|
+
push_path Regexp.new(parsed_value)
|
322
|
+
else
|
323
|
+
push_path parsed_value.to_s
|
324
|
+
end
|
325
|
+
|
326
|
+
location_key = @current_path.join(".")
|
327
|
+
@locations[location_key] = node.location
|
328
|
+
|
329
|
+
current_context[:key] = node
|
330
|
+
else
|
331
|
+
# We are processing a value
|
332
|
+
|
333
|
+
current_context[:mapping] << [current_context[:key], node]
|
334
|
+
current_context[:key] = nil
|
335
|
+
pop_path
|
336
|
+
end
|
337
|
+
return
|
338
|
+
end
|
339
|
+
|
340
|
+
raise Argot::Unprocessable, "Unknown context in scalar"
|
341
|
+
end
|
342
|
+
|
343
|
+
def start_sequence(anchor, tag, implicit, style)
|
344
|
+
@context.push({sequence: [], tag: tag})
|
345
|
+
end
|
346
|
+
|
347
|
+
def end_sequence
|
348
|
+
sequence = @context.pop
|
349
|
+
|
350
|
+
node = Node.new(
|
351
|
+
value: sequence[:sequence],
|
352
|
+
tag: sequence[:tag],
|
353
|
+
location: @current_location.dup,
|
354
|
+
path: dup_path
|
355
|
+
)
|
356
|
+
|
357
|
+
if context_empty?
|
358
|
+
@tree = node
|
359
|
+
return
|
360
|
+
end
|
361
|
+
|
362
|
+
if in_sequence?
|
363
|
+
current_context[:sequence] << node
|
364
|
+
elsif in_mapping?
|
365
|
+
if current_context[:key].nil?
|
366
|
+
raise Argot::Unprocessable, "Unexpected state in end_sequence"
|
367
|
+
else
|
368
|
+
current_context[:mapping] << [current_context[:key], node]
|
369
|
+
current_context[:key] = nil
|
370
|
+
pop_path
|
371
|
+
end
|
372
|
+
else
|
373
|
+
raise Argot::Unprocessable, "Unknown context in end_sequence"
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
private
|
378
|
+
|
379
|
+
def parse_scalar(value)
|
380
|
+
@scalar_scanner.tokenize(value)
|
381
|
+
rescue ArgumentError, Psych::SyntaxError
|
382
|
+
value.to_s
|
383
|
+
end
|
384
|
+
end
|
385
|
+
end
|
386
|
+
end
|
data/lib/argot/tag.rb
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
module Argot
|
2
|
+
module Tag
|
3
|
+
@tags = {}
|
4
|
+
@global_uri = "tag:argot.packfiles.io,2024"
|
5
|
+
|
6
|
+
def self.register(klass)
|
7
|
+
full_tag = "#{klass.uri}:#{klass.tag_name}"
|
8
|
+
@tags[full_tag] = Object.const_get(klass.name)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.for(tag)
|
12
|
+
@tags[tag]&.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.tags
|
16
|
+
@tags
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.global_uri
|
20
|
+
@global_uri
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.global_uri=(uri)
|
24
|
+
@global_uri = uri
|
25
|
+
end
|
26
|
+
|
27
|
+
class Base
|
28
|
+
class << self
|
29
|
+
attr_reader :annotation_type
|
30
|
+
|
31
|
+
def annotates_key?
|
32
|
+
@annotation_type == :key || @annotation_type == :key_or_value
|
33
|
+
end
|
34
|
+
|
35
|
+
def annotates_value?
|
36
|
+
@annotation_type == :value || @annotation_type == :key_or_value
|
37
|
+
end
|
38
|
+
|
39
|
+
def annotates(arg)
|
40
|
+
@annotation_type = arg if %i[key value key_or_value].include?(arg)
|
41
|
+
end
|
42
|
+
|
43
|
+
def tag_name(name = nil)
|
44
|
+
if name
|
45
|
+
@tag_name = name
|
46
|
+
Argot::Tag.register(self)
|
47
|
+
end
|
48
|
+
@tag_name
|
49
|
+
end
|
50
|
+
|
51
|
+
def uri(uri = nil)
|
52
|
+
return Argot::Tag.global_uri if uri.nil?
|
53
|
+
|
54
|
+
@uri = "tag:" + uri
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def annotation_type
|
59
|
+
self.class.annotation_type
|
60
|
+
end
|
61
|
+
|
62
|
+
def annotates_key?
|
63
|
+
self.class.annotates_key?
|
64
|
+
end
|
65
|
+
|
66
|
+
def annotates_value?
|
67
|
+
self.class.annotates_value?
|
68
|
+
end
|
69
|
+
|
70
|
+
def configure(value)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
Dir[File.join(__dir__, "tags", "*.rb")].each { |file| require file }
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Argot
|
2
|
+
module Tag
|
3
|
+
class Literal < Base
|
4
|
+
tag_name "literal"
|
5
|
+
tag_name "l"
|
6
|
+
annotates :value
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@expected_value = nil
|
10
|
+
end
|
11
|
+
|
12
|
+
def configure(value)
|
13
|
+
@expected_value = value
|
14
|
+
end
|
15
|
+
|
16
|
+
def expectation
|
17
|
+
"'#{@expected_value}'"
|
18
|
+
end
|
19
|
+
|
20
|
+
def validate(value)
|
21
|
+
value == @expected_value
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Argot
|
2
|
+
module Tag
|
3
|
+
class Pattern < Base
|
4
|
+
tag_name "rexpr"
|
5
|
+
tag_name "x"
|
6
|
+
annotates :key_or_value
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
super
|
10
|
+
@regex = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def configure(pattern)
|
14
|
+
@regex = Regexp.new(pattern)
|
15
|
+
end
|
16
|
+
|
17
|
+
def expectation
|
18
|
+
"a match for the regular expression '#{@regex.source}'"
|
19
|
+
end
|
20
|
+
|
21
|
+
def validate(value)
|
22
|
+
value.to_s.match?(@regex)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Argot
|
2
|
+
module Tag
|
3
|
+
class Range < Base
|
4
|
+
tag_name "range"
|
5
|
+
tag_name "rg"
|
6
|
+
annotates :value
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
super
|
10
|
+
@range = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def configure(range_str)
|
14
|
+
# Parse the range string using a regex
|
15
|
+
# Supports formats like '1..10', '1...10', '1-10', '1 to 10'
|
16
|
+
if range_str =~ /\A\s*(\d+)\s*(\.\.\.?|-|to)\s*(\d+)\s*\z/
|
17
|
+
start_num = Regexp.last_match(1).to_i
|
18
|
+
operator = Regexp.last_match(2)
|
19
|
+
end_num = Regexp.last_match(3).to_i
|
20
|
+
|
21
|
+
inclusive = ["..", "-", "to"].include?(operator)
|
22
|
+
exclusive = operator == "..."
|
23
|
+
|
24
|
+
@range = if inclusive
|
25
|
+
(start_num..end_num)
|
26
|
+
elsif exclusive
|
27
|
+
(start_num...end_num)
|
28
|
+
end
|
29
|
+
else
|
30
|
+
@range = nil
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def expectation
|
35
|
+
"a value between #{@range.min} and #{@range.max}"
|
36
|
+
end
|
37
|
+
|
38
|
+
def validate(value)
|
39
|
+
return false if @range.nil?
|
40
|
+
|
41
|
+
return false unless value.is_a?(Numeric)
|
42
|
+
|
43
|
+
@range.cover?(value)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Argot
|
2
|
+
module Tag
|
3
|
+
class Type < Base
|
4
|
+
tag_name "type"
|
5
|
+
tag_name "t"
|
6
|
+
annotates :value
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
super
|
10
|
+
@expected_types = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def configure(type_name)
|
14
|
+
@expected_types = case type_name.downcase
|
15
|
+
when "string"
|
16
|
+
[String]
|
17
|
+
when "integer"
|
18
|
+
[Integer]
|
19
|
+
when "float"
|
20
|
+
[Float]
|
21
|
+
when "array"
|
22
|
+
[Array]
|
23
|
+
when "hash", "map"
|
24
|
+
[Hash]
|
25
|
+
when "boolean"
|
26
|
+
[TrueClass, FalseClass]
|
27
|
+
else
|
28
|
+
[]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def expectation
|
33
|
+
return "Boolean" if @expected_types.length == 2
|
34
|
+
|
35
|
+
@expected_types.first.name
|
36
|
+
end
|
37
|
+
|
38
|
+
def validate(value)
|
39
|
+
return false if @expected_types.empty?
|
40
|
+
|
41
|
+
@expected_types.any? { |t| value.is_a?(t) }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,291 @@
|
|
1
|
+
module Argot
|
2
|
+
class Validator < Psych::Handler
|
3
|
+
attr_reader :errors
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def parse_and_validate(ruleset, file_content, file_name: "input")
|
7
|
+
handler = Argot::Validator.new(ruleset, file_name)
|
8
|
+
parser = Psych::Parser.new(handler)
|
9
|
+
parser.parse(file_content)
|
10
|
+
handler.validate_document
|
11
|
+
handler
|
12
|
+
end
|
13
|
+
|
14
|
+
def load(ruleset, file_content, file_name: "input", **)
|
15
|
+
handler = parse_and_validate(ruleset, file_content, file_name: file_name)
|
16
|
+
|
17
|
+
handler.safe_load(**)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(ruleset, file_name)
|
22
|
+
super()
|
23
|
+
@ruleset = ruleset
|
24
|
+
@file_name = file_name || "input"
|
25
|
+
@context = []
|
26
|
+
@current_path = []
|
27
|
+
@encountered_paths = {}
|
28
|
+
@missing_keys = {}
|
29
|
+
@errors = []
|
30
|
+
@current_location = {}
|
31
|
+
@scalar_scanner = build_safe_scalar_scanner
|
32
|
+
|
33
|
+
@stream_node = Psych::Nodes::Stream.new
|
34
|
+
@document_node = Psych::Nodes::Document.new([], [], true)
|
35
|
+
@stream_node.children << @document_node
|
36
|
+
end
|
37
|
+
|
38
|
+
def build_safe_scalar_scanner
|
39
|
+
class_loader = Psych::ClassLoader::Restricted.new([], [])
|
40
|
+
Psych::ScalarScanner.new(class_loader)
|
41
|
+
end
|
42
|
+
|
43
|
+
def errors?
|
44
|
+
!@errors.empty?
|
45
|
+
end
|
46
|
+
|
47
|
+
def tree
|
48
|
+
@stream_node
|
49
|
+
end
|
50
|
+
|
51
|
+
def safe_load(permitted_classes: [], permitted_symbols: [], aliases: false, filename: nil, fallback: nil, symbolize_names: false, freeze: false, strict_integer: false, ignore_validation_errors: false)
|
52
|
+
return nil if errors? && !ignore_validation_errors
|
53
|
+
return fallback unless @document_node
|
54
|
+
|
55
|
+
class_loader = Psych::ClassLoader::Restricted.new(permitted_classes.map(&:to_s), permitted_symbols.map(&:to_s))
|
56
|
+
|
57
|
+
scanner = Psych::ScalarScanner.new class_loader, strict_integer: strict_integer
|
58
|
+
|
59
|
+
visitor = if aliases
|
60
|
+
Psych::Visitors::ToRuby.new scanner, class_loader, symbolize_names: symbolize_names, freeze: freeze
|
61
|
+
else
|
62
|
+
Psych::Visitors::NoAliasRuby.new scanner, class_loader, symbolize_names: symbolize_names, freeze: freeze
|
63
|
+
end
|
64
|
+
|
65
|
+
begin
|
66
|
+
result = visitor.accept @document_node
|
67
|
+
rescue Psych::DisallowedClass => e
|
68
|
+
raise Argot::MaliciousInput, e.message
|
69
|
+
end
|
70
|
+
|
71
|
+
result
|
72
|
+
end
|
73
|
+
|
74
|
+
def event_location(start_line, start_column, end_line, end_column)
|
75
|
+
@current_location = {
|
76
|
+
start_line: start_line + 1,
|
77
|
+
end_line: end_line + 1,
|
78
|
+
start_column: start_column + 1,
|
79
|
+
end_column: end_column + 1
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
83
|
+
def start_mapping(anchor, tag, implicit, style)
|
84
|
+
mapping = Psych::Nodes::Mapping.new(anchor, tag, implicit, style)
|
85
|
+
push_context({node: mapping, key: nil})
|
86
|
+
end
|
87
|
+
|
88
|
+
def end_mapping
|
89
|
+
mapping = pop_context[:node]
|
90
|
+
|
91
|
+
if context_empty?
|
92
|
+
append_tree mapping
|
93
|
+
return
|
94
|
+
end
|
95
|
+
|
96
|
+
if in_sequence?
|
97
|
+
append_child mapping
|
98
|
+
elsif in_mapping?
|
99
|
+
raise Argot::Unprocessable, "Unexpected state in end_mapping" if current_context[:key].nil?
|
100
|
+
|
101
|
+
append_child current_context[:key]
|
102
|
+
append_child mapping
|
103
|
+
current_context[:key] = nil
|
104
|
+
pop_path
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def scalar(value, anchor, tag, plain, quoted, style)
|
109
|
+
scalar = Psych::Nodes::Scalar.new(value, anchor, tag, plain, quoted, style)
|
110
|
+
|
111
|
+
if in_sequence?
|
112
|
+
append_child scalar
|
113
|
+
validate_node scalar
|
114
|
+
pop_path
|
115
|
+
elsif in_mapping?
|
116
|
+
if current_context[:key].nil?
|
117
|
+
push_path value
|
118
|
+
current_context[:key] = scalar
|
119
|
+
else
|
120
|
+
append_child current_context[:key]
|
121
|
+
append_child scalar
|
122
|
+
validate_node scalar
|
123
|
+
|
124
|
+
current_context[:key] = nil
|
125
|
+
pop_path
|
126
|
+
end
|
127
|
+
else
|
128
|
+
raise Argot::Unprocessable, "Unknown context in scalar"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def start_sequence(anchor, tag, implicit, style)
|
133
|
+
sequence = Psych::Nodes::Sequence.new(anchor, tag, implicit, style)
|
134
|
+
push_context({node: sequence})
|
135
|
+
end
|
136
|
+
|
137
|
+
def end_sequence
|
138
|
+
sequence = pop_context[:node]
|
139
|
+
|
140
|
+
if context_empty?
|
141
|
+
append_tree sequence
|
142
|
+
elsif in_sequence?
|
143
|
+
append_child sequence
|
144
|
+
elsif in_mapping?
|
145
|
+
raise Argot::Unprocessable, "Unexpected state in end_sequence" if current_context[:key].nil?
|
146
|
+
|
147
|
+
append_child current_context[:key]
|
148
|
+
append_child sequence
|
149
|
+
current_context[:key] = nil
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def validate_document
|
154
|
+
# Check for required keys
|
155
|
+
@ruleset.paths.each do |ruleset_path|
|
156
|
+
parent_path = ruleset_path.take(ruleset_path.length - 1)
|
157
|
+
|
158
|
+
if @ruleset.required_path?(ruleset_path) && !encountered_path?(ruleset_path) && encountered_path?(parent_path)
|
159
|
+
emit_missing_key_error(ruleset_path)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# Check for optionally required keys (required keys that are children of an optional parent key)
|
164
|
+
@encountered_paths.each_key do |tracked_path|
|
165
|
+
subkey_paths = @ruleset.subkeys(tracked_path)
|
166
|
+
|
167
|
+
subkey_paths.each do |subkey_path_part|
|
168
|
+
subkey_path = tracked_path + subkey_path_part
|
169
|
+
rule_set = @ruleset.lookup(subkey_path)
|
170
|
+
|
171
|
+
next unless rule_set&.key?(:key)
|
172
|
+
|
173
|
+
emit_missing_key_error(subkey_path) if @ruleset.required_path?(subkey_path) && !encountered_path?(subkey_path)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
private
|
179
|
+
|
180
|
+
def in_sequence?
|
181
|
+
current_context[:node].is_a?(Psych::Nodes::Sequence)
|
182
|
+
end
|
183
|
+
|
184
|
+
def in_mapping?
|
185
|
+
current_context[:node].is_a?(Psych::Nodes::Mapping)
|
186
|
+
end
|
187
|
+
|
188
|
+
def append_child(node)
|
189
|
+
return unless @ruleset.permit_path?(@current_path)
|
190
|
+
|
191
|
+
@encountered_paths[@current_path.dup] = @current_location.dup unless @encountered_paths.key?(@current_path)
|
192
|
+
current_context[:node].children << node
|
193
|
+
end
|
194
|
+
|
195
|
+
def append_tree(node)
|
196
|
+
@document_node.children << node
|
197
|
+
end
|
198
|
+
|
199
|
+
def context_empty?
|
200
|
+
@context.empty?
|
201
|
+
end
|
202
|
+
|
203
|
+
def clear_context
|
204
|
+
@context = []
|
205
|
+
end
|
206
|
+
|
207
|
+
def push_context(val)
|
208
|
+
@context.push(val)
|
209
|
+
end
|
210
|
+
|
211
|
+
def pop_context
|
212
|
+
@context.pop
|
213
|
+
end
|
214
|
+
|
215
|
+
def current_context
|
216
|
+
@context.last
|
217
|
+
end
|
218
|
+
|
219
|
+
def push_path(val)
|
220
|
+
@current_path.push(val)
|
221
|
+
end
|
222
|
+
|
223
|
+
def pop_path
|
224
|
+
@current_path.pop
|
225
|
+
end
|
226
|
+
|
227
|
+
def parse_scalar(value)
|
228
|
+
@scalar_scanner.tokenize(value)
|
229
|
+
rescue ArgumentError, Psych::SyntaxError
|
230
|
+
value.to_s
|
231
|
+
end
|
232
|
+
|
233
|
+
def validate_node(node)
|
234
|
+
value = parse_scalar(node.value)
|
235
|
+
|
236
|
+
rule_set = @ruleset.lookup(@current_path)
|
237
|
+
|
238
|
+
if rule_set.nil?
|
239
|
+
@errors << Argot::Annotation.new(path: @file_name,
|
240
|
+
location: @current_location.dup,
|
241
|
+
level: Annotation::WARNING,
|
242
|
+
title: "Invalid Key",
|
243
|
+
message: "'#{@current_path.join(".")}' is not permitted in this document.")
|
244
|
+
return
|
245
|
+
end
|
246
|
+
|
247
|
+
return unless rule_set[:value].any?
|
248
|
+
|
249
|
+
validation_errors = []
|
250
|
+
valid = false
|
251
|
+
|
252
|
+
rule_set[:value].each do |tag|
|
253
|
+
validation_errors.append(tag.expectation)
|
254
|
+
valid = true if tag.validate(value)
|
255
|
+
end
|
256
|
+
|
257
|
+
return if valid
|
258
|
+
|
259
|
+
expectations = (validation_errors.size > 1) ? "#{validation_errors[0..-2].join(", ")}, or #{validation_errors[-1]}" : validation_errors.first
|
260
|
+
|
261
|
+
@errors << Argot::Annotation.new(path: @file_name,
|
262
|
+
location: @current_location.dup,
|
263
|
+
level: Annotation::FAILURE,
|
264
|
+
title: "Invalid Value",
|
265
|
+
message: "The value '#{value}' is invalid for #{@current_path.join(".")}",
|
266
|
+
details: "Expected #{expectations}")
|
267
|
+
end
|
268
|
+
|
269
|
+
def encountered_path?(subkey_path)
|
270
|
+
return true if subkey_path.empty?
|
271
|
+
|
272
|
+
@encountered_paths.keys.any? do |tracked_path|
|
273
|
+
@ruleset.match_path?(subkey_path, tracked_path)
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
def emit_missing_key_error(subkey_path)
|
278
|
+
return if @missing_keys.key?(subkey_path)
|
279
|
+
|
280
|
+
@errors << Argot::Annotation.new(
|
281
|
+
path: @file_name,
|
282
|
+
location: @encountered_paths[subkey_path] || @encountered_paths[subkey_path[..subkey_path.length - 2]] || {},
|
283
|
+
level: Argot::Annotation::FAILURE,
|
284
|
+
title: "Missing Key",
|
285
|
+
message: "The key '#{subkey_path.join(".")}' is required, but missing."
|
286
|
+
)
|
287
|
+
|
288
|
+
@missing_keys[subkey_path] = nil
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
data/lib/argot.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "psych"
|
4
|
+
require_relative "argot/version"
|
5
|
+
require_relative "argot/location"
|
6
|
+
require_relative "argot/annotation"
|
7
|
+
require_relative "argot/tag"
|
8
|
+
require_relative "argot/validator"
|
9
|
+
require_relative "argot/schema"
|
10
|
+
|
11
|
+
module Argot
|
12
|
+
class Error < StandardError; end
|
13
|
+
|
14
|
+
class Unprocessable < Error; end
|
15
|
+
|
16
|
+
class MaliciousInput < Unprocessable; end
|
17
|
+
end
|
data/sig/argot.rbs
ADDED
metadata
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: argot
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Charlton Trezevant
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-06-11 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: psych
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '5.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '5.1'
|
27
|
+
description: Quickly and flexibly build minimal, validatable YAML schemas.
|
28
|
+
email:
|
29
|
+
- charlton@packfiles.io
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- ".rubocop.yml"
|
35
|
+
- ".ruby-version"
|
36
|
+
- LICENSE.txt
|
37
|
+
- README.md
|
38
|
+
- Rakefile
|
39
|
+
- lib/argot.rb
|
40
|
+
- lib/argot/annotation.rb
|
41
|
+
- lib/argot/location.rb
|
42
|
+
- lib/argot/schema.rb
|
43
|
+
- lib/argot/tag.rb
|
44
|
+
- lib/argot/tags/literal.rb
|
45
|
+
- lib/argot/tags/oneof.rb
|
46
|
+
- lib/argot/tags/optional.rb
|
47
|
+
- lib/argot/tags/pattern.rb
|
48
|
+
- lib/argot/tags/range.rb
|
49
|
+
- lib/argot/tags/required.rb
|
50
|
+
- lib/argot/tags/type.rb
|
51
|
+
- lib/argot/validator.rb
|
52
|
+
- lib/argot/version.rb
|
53
|
+
- sig/argot.rbs
|
54
|
+
homepage: https://github.com/chtzvt/argot
|
55
|
+
licenses:
|
56
|
+
- MIT
|
57
|
+
metadata:
|
58
|
+
homepage_uri: https://github.com/chtzvt/argot
|
59
|
+
source_code_uri: https://github.com/chtzvt/argot
|
60
|
+
changelog_uri: https://github.com/chtzvt/argot
|
61
|
+
post_install_message:
|
62
|
+
rdoc_options: []
|
63
|
+
require_paths:
|
64
|
+
- lib
|
65
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 3.0.0
|
70
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
requirements: []
|
76
|
+
rubygems_version: 3.5.11
|
77
|
+
signing_key:
|
78
|
+
specification_version: 4
|
79
|
+
summary: Argot is a simple gem for quickly and flexibly building minimal, validatable
|
80
|
+
YAML schemas.
|
81
|
+
test_files: []
|