jimmy 0.5.3 → 2.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +35 -0
  3. data/.gitignore +4 -3
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +61 -0
  6. data/.ruby-version +1 -1
  7. data/.travis.yml +6 -0
  8. data/CODE_OF_CONDUCT.md +74 -0
  9. data/Gemfile +13 -0
  10. data/LICENSE +1 -1
  11. data/README.md +22 -134
  12. data/Rakefile +91 -1
  13. data/bin/console +15 -0
  14. data/bin/setup +8 -0
  15. data/jimmy.gemspec +25 -21
  16. data/lib/jimmy.rb +23 -15
  17. data/lib/jimmy/declaration.rb +150 -0
  18. data/lib/jimmy/declaration/assertion.rb +81 -0
  19. data/lib/jimmy/declaration/casting.rb +34 -0
  20. data/lib/jimmy/declaration/composites.rb +41 -0
  21. data/lib/jimmy/declaration/number.rb +27 -0
  22. data/lib/jimmy/declaration/object.rb +11 -0
  23. data/lib/jimmy/declaration/string.rb +98 -0
  24. data/lib/jimmy/declaration/types.rb +57 -0
  25. data/lib/jimmy/error.rb +9 -0
  26. data/lib/jimmy/file_map.rb +166 -0
  27. data/lib/jimmy/index.rb +78 -0
  28. data/lib/jimmy/json/array.rb +93 -0
  29. data/lib/jimmy/json/collection.rb +90 -0
  30. data/lib/jimmy/json/hash.rb +118 -0
  31. data/lib/jimmy/json/pointer.rb +119 -0
  32. data/lib/jimmy/json/uri.rb +144 -0
  33. data/lib/jimmy/loaders/base.rb +30 -0
  34. data/lib/jimmy/loaders/json.rb +15 -0
  35. data/lib/jimmy/loaders/ruby.rb +21 -0
  36. data/lib/jimmy/macros.rb +37 -0
  37. data/lib/jimmy/schema.rb +107 -87
  38. data/lib/jimmy/schema/array.rb +95 -0
  39. data/lib/jimmy/schema/casting.rb +17 -0
  40. data/lib/jimmy/schema/json.rb +40 -0
  41. data/lib/jimmy/schema/number.rb +47 -0
  42. data/lib/jimmy/schema/object.rb +108 -0
  43. data/lib/jimmy/schema/operators.rb +96 -0
  44. data/lib/jimmy/schema/string.rb +44 -0
  45. data/lib/jimmy/schema_with_uri.rb +53 -0
  46. data/lib/jimmy/schemer_factory.rb +65 -0
  47. data/lib/jimmy/version.rb +3 -1
  48. data/schema07.json +172 -0
  49. metadata +50 -101
  50. data/circle.yml +0 -11
  51. data/lib/jimmy/combination.rb +0 -34
  52. data/lib/jimmy/definitions.rb +0 -38
  53. data/lib/jimmy/domain.rb +0 -111
  54. data/lib/jimmy/link.rb +0 -93
  55. data/lib/jimmy/reference.rb +0 -39
  56. data/lib/jimmy/schema_creation.rb +0 -121
  57. data/lib/jimmy/schema_type.rb +0 -100
  58. data/lib/jimmy/schema_types.rb +0 -42
  59. data/lib/jimmy/schema_types/array.rb +0 -30
  60. data/lib/jimmy/schema_types/boolean.rb +0 -6
  61. data/lib/jimmy/schema_types/integer.rb +0 -8
  62. data/lib/jimmy/schema_types/null.rb +0 -6
  63. data/lib/jimmy/schema_types/number.rb +0 -34
  64. data/lib/jimmy/schema_types/object.rb +0 -45
  65. data/lib/jimmy/schema_types/string.rb +0 -40
  66. data/lib/jimmy/symbol_array.rb +0 -17
  67. data/lib/jimmy/transform_keys.rb +0 -39
  68. data/lib/jimmy/type_reference.rb +0 -14
  69. 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