jimmy 0.5.5 → 2.0.0

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 +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 -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,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