jimmy 0.5.3 → 2.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.circleci/config.yml +35 -0
- data/.gitignore +4 -3
- data/.rspec +3 -0
- data/.rubocop.yml +61 -0
- data/.ruby-version +1 -1
- data/.travis.yml +6 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +13 -0
- data/LICENSE +1 -1
- data/README.md +22 -134
- data/Rakefile +91 -1
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/jimmy.gemspec +25 -21
- data/lib/jimmy.rb +23 -15
- data/lib/jimmy/declaration.rb +150 -0
- data/lib/jimmy/declaration/assertion.rb +81 -0
- data/lib/jimmy/declaration/casting.rb +34 -0
- data/lib/jimmy/declaration/composites.rb +41 -0
- data/lib/jimmy/declaration/number.rb +27 -0
- data/lib/jimmy/declaration/object.rb +11 -0
- data/lib/jimmy/declaration/string.rb +98 -0
- data/lib/jimmy/declaration/types.rb +57 -0
- data/lib/jimmy/error.rb +9 -0
- data/lib/jimmy/file_map.rb +166 -0
- data/lib/jimmy/index.rb +78 -0
- data/lib/jimmy/json/array.rb +93 -0
- data/lib/jimmy/json/collection.rb +90 -0
- data/lib/jimmy/json/hash.rb +118 -0
- data/lib/jimmy/json/pointer.rb +119 -0
- data/lib/jimmy/json/uri.rb +144 -0
- data/lib/jimmy/loaders/base.rb +30 -0
- data/lib/jimmy/loaders/json.rb +15 -0
- data/lib/jimmy/loaders/ruby.rb +21 -0
- data/lib/jimmy/macros.rb +37 -0
- data/lib/jimmy/schema.rb +107 -87
- data/lib/jimmy/schema/array.rb +95 -0
- data/lib/jimmy/schema/casting.rb +17 -0
- data/lib/jimmy/schema/json.rb +40 -0
- data/lib/jimmy/schema/number.rb +47 -0
- data/lib/jimmy/schema/object.rb +108 -0
- data/lib/jimmy/schema/operators.rb +96 -0
- data/lib/jimmy/schema/string.rb +44 -0
- data/lib/jimmy/schema_with_uri.rb +53 -0
- data/lib/jimmy/schemer_factory.rb +65 -0
- data/lib/jimmy/version.rb +3 -1
- data/schema07.json +172 -0
- metadata +50 -101
- data/circle.yml +0 -11
- data/lib/jimmy/combination.rb +0 -34
- data/lib/jimmy/definitions.rb +0 -38
- data/lib/jimmy/domain.rb +0 -111
- data/lib/jimmy/link.rb +0 -93
- data/lib/jimmy/reference.rb +0 -39
- data/lib/jimmy/schema_creation.rb +0 -121
- data/lib/jimmy/schema_type.rb +0 -100
- data/lib/jimmy/schema_types.rb +0 -42
- data/lib/jimmy/schema_types/array.rb +0 -30
- data/lib/jimmy/schema_types/boolean.rb +0 -6
- data/lib/jimmy/schema_types/integer.rb +0 -8
- data/lib/jimmy/schema_types/null.rb +0 -6
- data/lib/jimmy/schema_types/number.rb +0 -34
- data/lib/jimmy/schema_types/object.rb +0 -45
- data/lib/jimmy/schema_types/string.rb +0 -40
- data/lib/jimmy/symbol_array.rb +0 -17
- data/lib/jimmy/transform_keys.rb +0 -39
- data/lib/jimmy/type_reference.rb +0 -14
- data/lib/jimmy/validation_error.rb +0 -20
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Jimmy
|
4
|
+
module Json
|
5
|
+
# Common methods for {Hash} and {Array}
|
6
|
+
module Collection
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
# Serialize the collection as JSON.
|
10
|
+
def to_json(**opts)
|
11
|
+
JSON.generate as_json, **opts
|
12
|
+
end
|
13
|
+
|
14
|
+
# @see Object#inspect
|
15
|
+
def inspect
|
16
|
+
to_json
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns true if the collection has no members.
|
20
|
+
# @return [true, false]
|
21
|
+
def empty?
|
22
|
+
@members.empty?
|
23
|
+
end
|
24
|
+
|
25
|
+
# Freeze the collection.
|
26
|
+
# @return [self]
|
27
|
+
def freeze
|
28
|
+
@members.freeze
|
29
|
+
super
|
30
|
+
end
|
31
|
+
|
32
|
+
# Get the member of the collection assigned to the given key.
|
33
|
+
def [](key)
|
34
|
+
@members[cast_key(key)]
|
35
|
+
end
|
36
|
+
|
37
|
+
# @see Hash#dig
|
38
|
+
def dig(key, *rest)
|
39
|
+
obj = self[cast_key(key)]
|
40
|
+
return obj if obj.nil? || rest.empty?
|
41
|
+
|
42
|
+
obj.dig(*rest)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Transform the collection into plain JSON-compatible objects.
|
46
|
+
# @return [Hash, Array]
|
47
|
+
def as_json(id: '', index: {})
|
48
|
+
return index[object_id].as_json(id: id, index: {}) if index[object_id]
|
49
|
+
|
50
|
+
id = Json::URI.new(id)
|
51
|
+
index[object_id] = Jimmy.ref(id)
|
52
|
+
|
53
|
+
pairs = map do |key, value|
|
54
|
+
if value.respond_to? :as_json
|
55
|
+
value = value.as_json(id: id / key, index: index)
|
56
|
+
end
|
57
|
+
[key, value]
|
58
|
+
end
|
59
|
+
|
60
|
+
export_pairs pairs
|
61
|
+
end
|
62
|
+
|
63
|
+
# Removes all members.
|
64
|
+
# @return [self]
|
65
|
+
def clear
|
66
|
+
@members.clear
|
67
|
+
self
|
68
|
+
end
|
69
|
+
|
70
|
+
protected
|
71
|
+
|
72
|
+
def cast_value(value)
|
73
|
+
case value
|
74
|
+
when nil, true, false, Numeric, String, Collection then value
|
75
|
+
when ::Hash then Hash.new(value)
|
76
|
+
when ::Array, Set then Array.new(value)
|
77
|
+
else
|
78
|
+
unless value.respond_to? :as_json
|
79
|
+
raise Error::WrongType, "Incompatible JSON type #{value.class}"
|
80
|
+
end
|
81
|
+
|
82
|
+
value.as_json
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
require 'jimmy/json/array'
|
90
|
+
require 'jimmy/json/hash'
|
@@ -0,0 +1,118 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'jimmy/json/collection'
|
4
|
+
require 'jimmy/json/pointer'
|
5
|
+
|
6
|
+
module Jimmy
|
7
|
+
module Json
|
8
|
+
# Represents a JSON object that is part of a JSON schema.
|
9
|
+
class Hash
|
10
|
+
include Collection
|
11
|
+
|
12
|
+
# @param [Hash, ::Hash] hash Items to be merged into the new hash.
|
13
|
+
def initialize(hash = {})
|
14
|
+
super()
|
15
|
+
@members = {}
|
16
|
+
merge! hash
|
17
|
+
end
|
18
|
+
|
19
|
+
# Set a value in the hash.
|
20
|
+
# @param [String, Symbol] key The key to set
|
21
|
+
# @param [Object] value The value to set
|
22
|
+
def []=(key, value)
|
23
|
+
key, value = cast(key, value)
|
24
|
+
@members[key] = value
|
25
|
+
sort!
|
26
|
+
end
|
27
|
+
|
28
|
+
# Fetch a value from the hash.
|
29
|
+
# @param [String, Symbol] key Key of the item to fetch
|
30
|
+
# @see ::Hash#fetch
|
31
|
+
# @return [Object]
|
32
|
+
def fetch(key, *args, &block)
|
33
|
+
@members.fetch cast_key(key), *args, &block
|
34
|
+
end
|
35
|
+
|
36
|
+
# Merge values of another hash into this hash.
|
37
|
+
# @param [Hash, ::Hash] hash
|
38
|
+
# @return [self]
|
39
|
+
def merge!(hash)
|
40
|
+
hash.each { |k, v| self[k] = v }
|
41
|
+
self
|
42
|
+
end
|
43
|
+
|
44
|
+
# Iterate over items in the hash. If a block with a single argument is
|
45
|
+
# given, only values will be yielded. Otherwise, keys and values will be
|
46
|
+
# yielded.
|
47
|
+
# @yieldparam [String] key The key of each item.
|
48
|
+
# @yieldparam [Object] member Each item.
|
49
|
+
# @return [Enumerable, self] If no block is given, an {::Enumerable} is
|
50
|
+
# returned. Otherwise, +self+ is returned.
|
51
|
+
def each(&block)
|
52
|
+
return enum_for :each unless block
|
53
|
+
|
54
|
+
@members.each do |key, value|
|
55
|
+
if block.arity == 1
|
56
|
+
yield value
|
57
|
+
else
|
58
|
+
yield key, value
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Returns true if the given key is assigned.
|
64
|
+
# @param [String, Symbol] key The key to check.
|
65
|
+
def key?(key)
|
66
|
+
@members.key? cast_key(key)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Get an array of all keys in the hash.
|
70
|
+
# @return [Array<String>]
|
71
|
+
def keys
|
72
|
+
@members.keys
|
73
|
+
end
|
74
|
+
|
75
|
+
# Get the JSON fragment for the given pointer. Returns nil if the pointer
|
76
|
+
# is unmatched.
|
77
|
+
# @param [Jimmy::Json::Pointer, String] json_pointer
|
78
|
+
# @return [Jimmy::Collection, nil]
|
79
|
+
def get_fragment(json_pointer)
|
80
|
+
json_pointer = Pointer.new(json_pointer)
|
81
|
+
return self if json_pointer.empty?
|
82
|
+
|
83
|
+
dig *json_pointer.to_a
|
84
|
+
end
|
85
|
+
|
86
|
+
protected
|
87
|
+
|
88
|
+
def export_pairs(pairs)
|
89
|
+
pairs.to_h
|
90
|
+
end
|
91
|
+
|
92
|
+
def sort!
|
93
|
+
return unless respond_to? :sort_keys_by
|
94
|
+
|
95
|
+
@members = @members.sort do |a, b|
|
96
|
+
sort_keys_by(*a) <=> sort_keys_by(*b)
|
97
|
+
end.to_h
|
98
|
+
end
|
99
|
+
|
100
|
+
def cast(key, value)
|
101
|
+
[
|
102
|
+
cast_key(key),
|
103
|
+
cast_value(value)
|
104
|
+
]
|
105
|
+
end
|
106
|
+
|
107
|
+
def cast_key(key)
|
108
|
+
key = key.to_s.gsub(/_(.)/) { $1.upcase } if key.is_a? Symbol
|
109
|
+
|
110
|
+
unless key.is_a? String
|
111
|
+
raise Error::WrongType, "Invalid hash key of type #{key.class}"
|
112
|
+
end
|
113
|
+
|
114
|
+
key.strip
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Jimmy
|
4
|
+
module Json
|
5
|
+
# Represents a JSON pointer per https://tools.ietf.org/html/rfc6901
|
6
|
+
class Pointer
|
7
|
+
ESCAPE = [%r{[~/]}, { '~' => '~0', '/' => '~1' }.freeze].freeze
|
8
|
+
UNESCAPE = [/~[01]/, ESCAPE.last.invert.freeze].freeze
|
9
|
+
|
10
|
+
# @param [::Array<String>, Pointer, String] path A string starting with
|
11
|
+
# a +/+ and in JSON pointer format, or an array of parts of the pointer.
|
12
|
+
def initialize(path)
|
13
|
+
@path =
|
14
|
+
case path
|
15
|
+
when ::Array, Pointer then path.to_a
|
16
|
+
when String then parse(path)
|
17
|
+
else
|
18
|
+
raise Error::WrongType, "Unexpected #{path.class}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# @return [Array<String>] The individual parts of the pointer.
|
23
|
+
def to_a
|
24
|
+
@path.dup
|
25
|
+
end
|
26
|
+
|
27
|
+
# Make a new pointer by appending +other+ to self.
|
28
|
+
# @param [Pointer, Integer, String, ::Array<String>] other The pointer to
|
29
|
+
# append.
|
30
|
+
# @return [Pointer]
|
31
|
+
def join(other)
|
32
|
+
if other.is_a? Integer
|
33
|
+
return shed(-other) if other.negative?
|
34
|
+
|
35
|
+
other = other.to_s
|
36
|
+
end
|
37
|
+
|
38
|
+
other = '/' + other if other.is_a?(String) && other[0] != '/'
|
39
|
+
self.class.new(@path + self.class.new(other).to_a)
|
40
|
+
end
|
41
|
+
|
42
|
+
alias + join
|
43
|
+
|
44
|
+
# Make a new pointer by removing +count+ parts from the end of self.
|
45
|
+
# @param [Integer] count
|
46
|
+
# @return [Pointer]
|
47
|
+
def shed(count)
|
48
|
+
unless count.is_a?(Integer) && !count.negative?
|
49
|
+
raise Error::BadArgument, 'Expected a non-negative integer'
|
50
|
+
end
|
51
|
+
return dup if count.zero?
|
52
|
+
raise Error::BadArgument, 'Out of range' if count > @path.length
|
53
|
+
|
54
|
+
self.class.new @path[0..(-count - 1)]
|
55
|
+
end
|
56
|
+
|
57
|
+
alias - shed
|
58
|
+
|
59
|
+
# Get the pointer as a string, either blank, or starting with a +/+.
|
60
|
+
# @return [String]
|
61
|
+
def to_s
|
62
|
+
return '' if @path.empty?
|
63
|
+
|
64
|
+
@path.map { |str| '/' + str.gsub(*ESCAPE) }.join
|
65
|
+
end
|
66
|
+
|
67
|
+
# Returns true if +other+ has the same string value as self.
|
68
|
+
# @param [Pointer] other
|
69
|
+
# @return [true, false]
|
70
|
+
def ==(other)
|
71
|
+
other.is_a?(self.class) && @path == other.to_a
|
72
|
+
end
|
73
|
+
|
74
|
+
# @see ::Object#inspect
|
75
|
+
def inspect
|
76
|
+
"#<#{self.class} #{self}>"
|
77
|
+
end
|
78
|
+
|
79
|
+
# Returns true if the pointer has no parts.
|
80
|
+
# @return [true, false]
|
81
|
+
def empty?
|
82
|
+
@path.empty?
|
83
|
+
end
|
84
|
+
|
85
|
+
# Remove the last part of the pointer.
|
86
|
+
# @return [String] The part that was removed.
|
87
|
+
def shift
|
88
|
+
@path.shift
|
89
|
+
end
|
90
|
+
|
91
|
+
# Return a new pointer with just the part of self that is not included
|
92
|
+
# in +other+.
|
93
|
+
#
|
94
|
+
# Jimmy::Json::Pointer.new('/foo/bar/baz').remove_prefix('/foo')
|
95
|
+
# # => #<Jimmy::Json::Pointer /bar/baz>
|
96
|
+
# @param [String, Pointer, ::Array<String>] other
|
97
|
+
def remove_prefix(other)
|
98
|
+
tail = dup
|
99
|
+
Pointer.new(other).to_a.each do |segment|
|
100
|
+
return nil unless tail.shift == segment
|
101
|
+
end
|
102
|
+
tail
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def parse(path)
|
108
|
+
return [] if path == ''
|
109
|
+
return [''] if path == '/'
|
110
|
+
|
111
|
+
unless path[0] == '/'
|
112
|
+
raise Error::BadArgument, 'JSON pointers should start with /'
|
113
|
+
end
|
114
|
+
|
115
|
+
path[1..].split('/').map { |str| str.gsub *UNESCAPE }
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'jimmy/json/pointer'
|
4
|
+
|
5
|
+
module Jimmy
|
6
|
+
module Json
|
7
|
+
# Wraps the URI class to provide additional functionality.
|
8
|
+
class URI
|
9
|
+
# Take from the back of URI::RFC3986_Parser::RFC3986_URI
|
10
|
+
FRAGMENT_ESCAPING = %r{[^!$&-.0-;=@-Z_a-z~/?]}.freeze
|
11
|
+
|
12
|
+
# @param [String, URI, ::URI] uri
|
13
|
+
# @param [true, false] container If true, a +/+ will be appended to the
|
14
|
+
# given +uri+ if it was omitted. Otherwise, a +#+ will be appended.
|
15
|
+
def initialize(uri, container: false)
|
16
|
+
@uri = ::URI.parse(uri.to_s)
|
17
|
+
if container
|
18
|
+
@uri.path += '/' unless @uri.path.end_with? '/'
|
19
|
+
else
|
20
|
+
@uri.fragment ||= ''
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Get the fragment of this URI as a {Pointer}.
|
25
|
+
# @return [Pointer]
|
26
|
+
def pointer
|
27
|
+
Pointer.new ::URI.decode_www_form_component(fragment)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Set the fragment of this URI using a {Pointer}.
|
31
|
+
# @param [String, Pointer, ::Array<String>] value
|
32
|
+
def pointer=(value)
|
33
|
+
# Loosely based on URI.encode_www_form_component
|
34
|
+
fragment = Pointer.new(value).to_s.dup
|
35
|
+
fragment.force_encoding Encoding::ASCII_8BIT
|
36
|
+
fragment.gsub!(FRAGMENT_ESCAPING) { |chr| '%%%02X' % chr.ord }
|
37
|
+
fragment.force_encoding Encoding::US_ASCII
|
38
|
+
self.fragment = fragment
|
39
|
+
end
|
40
|
+
|
41
|
+
undef to_s
|
42
|
+
|
43
|
+
# @see ::Object#inspect
|
44
|
+
def inspect
|
45
|
+
"#<#{self.class} #{self}>"
|
46
|
+
end
|
47
|
+
|
48
|
+
# Returns true if +other+ represents the same URI as self.
|
49
|
+
# @param [URI] other
|
50
|
+
def ==(other)
|
51
|
+
other.is_a?(self.class) && other.to_s == to_s
|
52
|
+
end
|
53
|
+
|
54
|
+
alias eql? ==
|
55
|
+
|
56
|
+
# @see ::URI#join
|
57
|
+
def join(other)
|
58
|
+
self.class.new(@uri + other.to_s)
|
59
|
+
end
|
60
|
+
|
61
|
+
alias + join
|
62
|
+
|
63
|
+
# Return a new URI with the given pointer appended.
|
64
|
+
# @param [Pointer, String, ::Array<String>] other
|
65
|
+
def /(other)
|
66
|
+
dup.tap { |uri| uri.pointer += other }
|
67
|
+
end
|
68
|
+
|
69
|
+
# @see ::Object#dup
|
70
|
+
# @return [URI]
|
71
|
+
def dup
|
72
|
+
self.class.new self
|
73
|
+
end
|
74
|
+
|
75
|
+
# @api private
|
76
|
+
def hash
|
77
|
+
[self.class, @uri].hash
|
78
|
+
end
|
79
|
+
|
80
|
+
# Get this URI as a string. If +id+ is given, the string will be this URI
|
81
|
+
# relative to the given URI.
|
82
|
+
#
|
83
|
+
# uri = Jimmy::Json::URI.new('http://example.com/foo/bar#')
|
84
|
+
# uri.as_json(id: 'http://example.com/foo/')
|
85
|
+
# # => "bar#"
|
86
|
+
# @param [URI, ::URI, String] id If not nil, the URI will be represented
|
87
|
+
# relative to +id+.
|
88
|
+
def as_json(id: nil, **)
|
89
|
+
return to_s unless id
|
90
|
+
|
91
|
+
id = URI.new(id)
|
92
|
+
id.absolute? ? id.route_to(id + self).to_s : to_s
|
93
|
+
end
|
94
|
+
|
95
|
+
# @see URI::Generic#route_to
|
96
|
+
# @param [Json::URI, URI, String] other
|
97
|
+
# @return [Json::URI]
|
98
|
+
def route_to(other)
|
99
|
+
self.class.new(@uri.route_to other.to_s)
|
100
|
+
end
|
101
|
+
|
102
|
+
# @!method fragment
|
103
|
+
# @see URI::Generic#fragment
|
104
|
+
# @return [String]
|
105
|
+
# @!method path
|
106
|
+
# @see URI::Generic#path
|
107
|
+
# @return [String]
|
108
|
+
# @!method host
|
109
|
+
# @see URI::Generic#host
|
110
|
+
# @return [String]
|
111
|
+
# @!method relative?
|
112
|
+
# @see URI::Generic#relative?
|
113
|
+
# @return [true, false]
|
114
|
+
# @!method absolute?
|
115
|
+
# @see URI::Generic#absolute?
|
116
|
+
# @return [true, false]
|
117
|
+
# @!method path=(value)
|
118
|
+
# @see URI::Generic#path=
|
119
|
+
# @param [String] value
|
120
|
+
# @!method fragment=(value)
|
121
|
+
# @see URI::Generic#fragment=
|
122
|
+
# @param [String] value
|
123
|
+
|
124
|
+
# @api private
|
125
|
+
def respond_to_missing?(name, *)
|
126
|
+
@uri.respond_to? name or super
|
127
|
+
end
|
128
|
+
|
129
|
+
# @api private
|
130
|
+
def method_missing(symbol, *args, &block)
|
131
|
+
if @uri.respond_to? symbol
|
132
|
+
self.class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
133
|
+
def #{symbol}(*args, &block)
|
134
|
+
@uri.__send__ :#{symbol}, *args, &block
|
135
|
+
end
|
136
|
+
RUBY
|
137
|
+
|
138
|
+
return __send__ symbol, *args, &block
|
139
|
+
end
|
140
|
+
super
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|