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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jimmy
4
+ module Declaration
5
+ # Shortcut for +object.additional_properties(false)+.
6
+ # @return [Jimmy::Schema]
7
+ def struct
8
+ object.additional_properties false
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jimmy
4
+ module Declaration
5
+ FORMATS = Set.new(
6
+ %w[
7
+ date-time
8
+ date
9
+ time
10
+ email
11
+ idn-email
12
+ hostname
13
+ idn-hostname
14
+ ipv4
15
+ ipv6
16
+ uri
17
+ uri-reference
18
+ iri
19
+ iri-reference
20
+ uri-template
21
+ json-pointer
22
+ relative-json-pointer
23
+ regex
24
+ ]
25
+ ).freeze
26
+
27
+ # Set the pattern for a string value.
28
+ # @param [Regexp] expression The pattern for a string value. Cannot include
29
+ # any options such as +/i+.
30
+ # @return [self] self, for chaining
31
+ def pattern(expression)
32
+ assert_regexp expression
33
+ string
34
+ set pattern: expression.source
35
+ end
36
+
37
+ FORMATS.each do |format|
38
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
39
+ def #{format.gsub '-', '_'}
40
+ string
41
+ format '#{format}'
42
+ end
43
+ RUBY
44
+ end
45
+
46
+ # Generated by `rake yard`. Do not modify anything below here.
47
+ #
48
+ # @!method date_time()
49
+ # Validate a string with format "date-time".
50
+ # @return [Schema]
51
+ # @!method date()
52
+ # Validate a string with format "date".
53
+ # @return [Schema]
54
+ # @!method time()
55
+ # Validate a string with format "time".
56
+ # @return [Schema]
57
+ # @!method email()
58
+ # Validate a string with format "email".
59
+ # @return [Schema]
60
+ # @!method idn_email()
61
+ # Validate a string with format "idn-email".
62
+ # @return [Schema]
63
+ # @!method hostname()
64
+ # Validate a string with format "hostname".
65
+ # @return [Schema]
66
+ # @!method idn_hostname()
67
+ # Validate a string with format "idn-hostname".
68
+ # @return [Schema]
69
+ # @!method ipv4()
70
+ # Validate a string with format "ipv4".
71
+ # @return [Schema]
72
+ # @!method ipv6()
73
+ # Validate a string with format "ipv6".
74
+ # @return [Schema]
75
+ # @!method uri()
76
+ # Validate a string with format "uri".
77
+ # @return [Schema]
78
+ # @!method uri_reference()
79
+ # Validate a string with format "uri-reference".
80
+ # @return [Schema]
81
+ # @!method iri()
82
+ # Validate a string with format "iri".
83
+ # @return [Schema]
84
+ # @!method iri_reference()
85
+ # Validate a string with format "iri-reference".
86
+ # @return [Schema]
87
+ # @!method uri_template()
88
+ # Validate a string with format "uri-template".
89
+ # @return [Schema]
90
+ # @!method json_pointer()
91
+ # Validate a string with format "json-pointer".
92
+ # @return [Schema]
93
+ # @!method relative_json_pointer()
94
+ # Validate a string with format "relative-json-pointer".
95
+ # @return [Schema]
96
+ # @!method regex()
97
+ # Validate a string with format "regex".
98
+ # @return [Schema]
99
+ end
100
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jimmy
4
+ module Declaration
5
+ # Acceptable values for +#type+.
6
+ SIMPLE_TYPES =
7
+ Set.new(%w[array boolean integer null number object string]).freeze
8
+
9
+ # Set the type(s) of the schema.
10
+ # @param [String, Array<String>] types The type(s) of the schema.
11
+ # @return [self] self, for chaining
12
+ def type(*types)
13
+ types = types.flatten
14
+ types.each &method(:assert_simple_type)
15
+ assert_array types, unique: true, minimum: 1
16
+ types = Array(get('type') { [] }) | types.flatten
17
+ types = types.first if types.one?
18
+ set type: types
19
+ end
20
+
21
+ alias types type
22
+
23
+ SIMPLE_TYPES.each do |type|
24
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
25
+ def #{type}
26
+ type '#{type}'
27
+ end
28
+ RUBY
29
+ end
30
+
31
+ alias nullable null
32
+
33
+ # Generated by `rake yard`. Do not modify anything below here.
34
+ #
35
+ # @!method array()
36
+ # Make the schema allow type "array".
37
+ # @return [Schema]
38
+ # @!method boolean()
39
+ # Make the schema allow type "boolean".
40
+ # @return [Schema]
41
+ # @!method integer()
42
+ # Make the schema allow type "integer".
43
+ # @return [Schema]
44
+ # @!method null()
45
+ # Make the schema allow type "null".
46
+ # @return [Schema]
47
+ # @!method number()
48
+ # Make the schema allow type "number".
49
+ # @return [Schema]
50
+ # @!method object()
51
+ # Make the schema allow type "object".
52
+ # @return [Schema]
53
+ # @!method string()
54
+ # Make the schema allow type "string".
55
+ # @return [Schema]
56
+ end
57
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jimmy
4
+ class Error < StandardError
5
+ class InvalidSchemaPropertyValue < self; end
6
+ class WrongType < self; end
7
+ class BadArgument < self; end
8
+ end
9
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jimmy/index'
4
+ require 'jimmy/loaders/ruby'
5
+ require 'jimmy/loaders/json'
6
+ require 'jimmy/schema_with_uri'
7
+ require 'jimmy/schemer_factory'
8
+
9
+ module Jimmy
10
+ # Maps a directory of files to schemas with URIs. Can be used as a URI
11
+ # resolver with {SchemerFactory}.
12
+ #
13
+ # Given +~/schemas/user.rb+ as a schema file:
14
+ #
15
+ # file_map = FileMap.new('~/schemas', 'http://example.com/schemas/', suffix: '.json')
16
+ # file_map.resolve('user.json') # => SchemaWithURI
17
+ #
18
+ # Calling {SchemaWithURI#as_json} on the above will include the full ID
19
+ # +http://example.com/schemas/user.json#+ in the +$id+ key.
20
+ #
21
+ # Including the suffix in the call to {FileMap#resolve} is optional.
22
+ #
23
+ # If you initialize a {FileMap} with +live: true+, files will be loaded
24
+ # lazily and repeatedly, every time {FileMap#resolve} or {FileMap#index} is
25
+ # called. This is intended as convenience for development environments.
26
+ class FileMap
27
+ DEFAULT_LOADERS = {
28
+ 'rb' => Loaders::Ruby,
29
+ 'json' => Loaders::JSON
30
+ }.freeze
31
+
32
+ # @param [Pathname, String] base_dir The directory that should map to the
33
+ # given URI.
34
+ # @param [Json::URI, URI, String] base_uri The URI that should resolve to
35
+ # the given directory. If omitted, a +file://+ URI will be used for the
36
+ # absolute path of the given directory.
37
+ # @param [true, false] live When true, schemas are not stored in memory, and
38
+ # are instead live-reloaded on demand. Typically only useful for
39
+ # development environments.
40
+ # @param [Hash{String => #call}] loaders Loaders for one or more file types.
41
+ # By default, +.json+ files are parsed as-is, and +.rb+ files are
42
+ # evaluated in the context of a {Loaders::Ruby} instance, which
43
+ # exposes the methods in {Macros}.
44
+ # @param [String] suffix Optional suffix that will be appended to each
45
+ # schema ID. This can be set to +".json"+ if, for example, you want your
46
+ # schemas to have +.json+ suffixes when you serve them over HTTP.
47
+ def initialize(
48
+ base_dir,
49
+ base_uri = nil,
50
+ live: false,
51
+ loaders: DEFAULT_LOADERS,
52
+ suffix: ''
53
+ )
54
+ @dir = Pathname(base_dir).realpath
55
+ unless @dir.directory? && @dir.readable?
56
+ raise Error::BadArgument, 'Expected a readable directory'
57
+ end
58
+
59
+ base_uri ||= uri_for_dir
60
+ @uri = Json::URI.new(base_uri.to_s, container: true)
61
+
62
+ @live = live
63
+ @loaders = loaders
64
+ @suffix = suffix
65
+
66
+ index unless live
67
+ end
68
+
69
+ # Given a URI, either absolute or relative to the file map's base URI,
70
+ # returns a {SchemaWithURI} if a matching schema is found.
71
+ # @param [Json::URI, URI, String] uri
72
+ # @return [Jimmy::SchemaWithURI, nil]
73
+ def resolve(uri)
74
+ uri = make_child_uri(uri)
75
+ absolute_uri = @uri + uri
76
+
77
+ return index.resolve(absolute_uri) unless live?
78
+
79
+ schema = load_file(path_for_uri uri)&.get_fragment(uri.fragment)
80
+ schema && SchemaWithURI.new(absolute_uri, schema)
81
+ end
82
+
83
+ alias [] resolve
84
+
85
+ # Get an index of all schemas in the file map's directory.
86
+ # @return [Jimmy::Index]
87
+ def index
88
+ return @index if @index
89
+
90
+ index = build_index
91
+ @index = index unless live?
92
+
93
+ index
94
+ end
95
+
96
+ # Returns true if live-reloading is enabled.
97
+ # @return [true, false]
98
+ def live?
99
+ @live
100
+ end
101
+
102
+ private
103
+
104
+ def load_file(file_base)
105
+ @loaders.each do |ext, loader|
106
+ file = Pathname("#{file_base}.#{ext}")
107
+ next unless file.file?
108
+ return loader.call(file) if file.readable?
109
+
110
+ warn "Jimmy cannot read #{file}"
111
+ end
112
+ nil
113
+ end
114
+
115
+ def uri_for_dir
116
+ Json::URI.new 'file://' + fs_to_rfc3968(@dir)
117
+ end
118
+
119
+ def path_for_uri(uri)
120
+ path = uri.path[0..(-@suffix.length - 1)]
121
+ parts = path.split('/').map(&URI.method(:decode_www_form_component))
122
+ @dir.join(*parts)
123
+ end
124
+
125
+ def make_child_uri(uri)
126
+ uri = @uri.route_to(@uri + uri)
127
+
128
+ unless uri.host.nil? && !uri.path.match?(%r{\A(\.\.|/)})
129
+ raise Error::BadArgument, 'The given URI is outside this FileMap'
130
+ end
131
+
132
+ uri.path += @suffix unless uri.path.end_with? @suffix
133
+ uri
134
+ end
135
+
136
+ def build_index
137
+ index = Index.new
138
+
139
+ Dir[@dir + "**/*.{#{@loaders.keys.join ','}}"].sort.each do |file|
140
+ relative_uri = relative_uri_for_file(file)
141
+ uri = @uri + relative_uri
142
+
143
+ index[uri] = @loaders.fetch(File.extname(file)[1..]).call(file)
144
+ end
145
+
146
+ index
147
+ end
148
+
149
+ def relative_uri_for_file(file)
150
+ path = Pathname(file)
151
+ .relative_path_from(@dir)
152
+ .to_s
153
+ .sub(/\.[^.]+\z/, '')
154
+
155
+ Json::URI.new fs_to_rfc3968(path) + @suffix
156
+ end
157
+
158
+ def fs_to_rfc3968(path)
159
+ path
160
+ .to_s
161
+ .split(File::SEPARATOR)
162
+ .map { |p| URI.encode_www_form_component p.sub(/:\z/, '') }
163
+ .join('/')
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jimmy/schema'
4
+ require 'jimmy/schema_with_uri'
5
+
6
+ module Jimmy
7
+ # Represents an in-memory collection of schemas
8
+ class Index
9
+ include Enumerable
10
+
11
+ def initialize # rubocop:disable Style/DocumentationMethod
12
+ @by_uri = {}
13
+ end
14
+
15
+ # @param [Json::URI, URI, String] uri
16
+ # @return [Jimmy::SchemaWithURI, nil]
17
+ def resolve(uri)
18
+ uri = Json::URI.new(uri)
19
+ raise Error::BadArgument, 'Cannot resolve relative URIs' if uri.relative?
20
+
21
+ return @by_uri[uri] if @by_uri.key? uri
22
+ return if uri.pointer.empty?
23
+
24
+ resolve(uri / -1)&.resolve uri
25
+ end
26
+
27
+ alias [] resolve
28
+
29
+ # @param [Json::URI, URI, String] uri
30
+ # @param [Jimmy::Schema] schema
31
+ # @return [self] self, for chaining
32
+ def add(uri, schema)
33
+ uri = Json::URI.new(uri)
34
+ raise Error::BadArgument, 'Expected a schema' unless schema.is_a? Schema
35
+ raise Error::BadArgument, 'Cannot index relative URIs' if uri.relative?
36
+
37
+ push SchemaWithURI.new(uri, schema)
38
+ end
39
+
40
+ alias []= add
41
+
42
+ # @param [Array<Jimmy::SchemaWithURI>] schemas_with_uris
43
+ # @return [self]
44
+ def push(*schemas_with_uris)
45
+ schemas_with_uris.each do |schema_with_uri|
46
+ unless schema_with_uri.is_a? SchemaWithURI
47
+ raise Error::BadArgument, 'Expected a SchemaWithURI'
48
+ end
49
+
50
+ @by_uri[schema_with_uri.uri] = schema_with_uri
51
+ end
52
+ self
53
+ end
54
+
55
+ alias << push
56
+
57
+ # @return [Array<Json::URI>]
58
+ def uris
59
+ @by_uri.keys
60
+ end
61
+
62
+ alias keys uris
63
+
64
+ # @param [Json::URI, URI, String] uri
65
+ # @return [true, false]
66
+ def uri?(uri)
67
+ !resolve(uri).nil?
68
+ end
69
+
70
+ alias key? uri?
71
+ alias has_key? key?
72
+
73
+ # @yieldparam [Jimmy::SchemaWithURI] schema_with_uri
74
+ def each(&block)
75
+ @by_uri.each_value &block
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jimmy/json/collection'
4
+
5
+ module Jimmy
6
+ module Json
7
+ # Represents an array in a JSON schema.
8
+ class Array
9
+ include Collection
10
+
11
+ KEY_PATTERN = /\A(?:\d|[1-9]\d+)\z/.freeze
12
+
13
+ # @param [Array, ::Array, Set] array Items to be included in the array.
14
+ def initialize(array = [])
15
+ super()
16
+ @members = []
17
+ concat array
18
+ end
19
+
20
+ # Append items in +array+ to self.
21
+ # @param [Array, ::Array, Set] array
22
+ # @return [self]
23
+ def concat(array)
24
+ array = array.to_a if array.is_a? Set
25
+ push *array
26
+ end
27
+
28
+ # Add one or more items to self.
29
+ # @param [Array] members Things to add.
30
+ # @return [self]
31
+ def push(*members)
32
+ @members.concat members.map(&method(:cast_value))
33
+ self
34
+ end
35
+
36
+ alias << push
37
+
38
+ # Iterate over items in the array. If a block with a single argument is
39
+ # given, only values will be yielded. Otherwise, indexes and values will
40
+ # be yielded.
41
+ # @yieldparam [Integer] index The index of each item.
42
+ # @yieldparam [Object] member Each item.
43
+ # @return [Enumerable, self] If no block is given, an {::Enumerable} is
44
+ # returned. Otherwise, +self+ is returned.
45
+ def each(&block)
46
+ return enum_for :each unless block
47
+
48
+ if block.arity == 1
49
+ @members.each { |member| yield member }
50
+ else
51
+ @members.each.with_index { |member, i| yield i, member }
52
+ end
53
+ self
54
+ end
55
+
56
+ # @return [Integer] The length of the array.
57
+ def length
58
+ @members.length
59
+ end
60
+
61
+ # Dig into the array.
62
+ # @param [Integer] key Index of the item to be dug or returned.
63
+ # @param [Array<String, Integer>] rest Keys or indexes to be passed to
64
+ # resolved hashes/arrays.
65
+ def dig(key, *rest)
66
+ key = key.to_i if key.is_a?(String) && key.match?(KEY_PATTERN)
67
+ super key, *rest
68
+ end
69
+
70
+ # @return [Array] Get a regular array.
71
+ def to_a
72
+ @members.dup
73
+ end
74
+
75
+ alias count length
76
+ alias size length
77
+
78
+ protected
79
+
80
+ def export_pairs(pairs)
81
+ pairs.map &:last
82
+ end
83
+
84
+ def cast_key(key)
85
+ unless key.is_a? Integer
86
+ raise Error::WrongType, "Invalid array index of type #{key.class}"
87
+ end
88
+
89
+ key
90
+ end
91
+ end
92
+ end
93
+ end