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.
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 +100 -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 +106 -86
  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 -10
  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