jimmy 0.5.1 → 2.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 +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 +100 -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 +106 -86
- 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 -10
- 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
|