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,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'jimmy'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -1,28 +1,32 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'jimmy/version'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/jimmy/version'
5
4
 
6
5
  Gem::Specification.new do |spec|
7
- spec.name = 'jimmy'
8
- spec.version = Jimmy::VERSION
9
- spec.authors = ['Neil E. Pearson']
10
- spec.email = ['neil.pearson@orionvm.com']
6
+ spec.name = 'jimmy'
7
+ spec.version = Jimmy::VERSION
8
+ spec.authors = ['Neil E. Pearson']
9
+ spec.email = ['neil@helium.net.au']
10
+
11
+ spec.summary = 'Jimmy the Gem'
12
+ spec.description = 'Jimmy the Gem'
13
+ spec.homepage = 'https://github.com/hx/jimmy'
14
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.3.0')
11
15
 
12
- spec.summary = 'Jimmy the JSON Schema DSL'
13
- spec.description = 'Jimmy makes it a snap to compose detailed JSON schema documents.'
14
- spec.homepage = 'https://github.com/hx/jimmy'
16
+ spec.metadata['homepage_uri'] = spec.homepage
17
+ spec.metadata['source_code_uri'] = spec.homepage
18
+ spec.metadata['changelog_uri'] = spec.homepage
15
19
 
16
- spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
- spec.bindir = 'bin'
18
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
20
+ # Specify which files should be added to the gem when it is released.
21
+ # The `git ls-files -z` loads the files in the RubyGem that have been added
22
+ # into git.
23
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
24
+ `git ls-files -z`
25
+ .split("\x0")
26
+ .reject { |f| f.start_with? 'spec/' }
27
+ end
28
+ spec.bindir = 'exe'
29
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
30
  spec.require_paths = ['lib']
20
31
  spec.license = 'Apache License, Version 2.0'
21
-
22
- spec.add_dependency 'json-schema', '~> 2.5'
23
-
24
- spec.add_development_dependency 'bundler', '~> 1.9'
25
- spec.add_development_dependency 'rake', '~> 10.0'
26
- spec.add_development_dependency 'rspec', '~> 3.2'
27
- spec.add_development_dependency 'diff_matcher', '~> 2.7'
28
32
  end
@@ -1,20 +1,28 @@
1
- require 'pathname'
1
+ # frozen_string_literal: true
2
+
3
+ require 'jimmy/error'
2
4
  require 'jimmy/version'
5
+ require 'jimmy/schema'
6
+ require 'jimmy/macros'
7
+ require 'jimmy/file_map'
8
+ require 'jimmy/json/uri'
3
9
 
10
+ # Jimmy makes declaring and validating against JSON schemas a piece of cake.
4
11
  module Jimmy
5
- ROOT = Pathname(__FILE__).parent.parent
6
- end
12
+ ROOT = Pathname(__dir__).parent
7
13
 
8
- require_relative 'jimmy/symbol_array'
14
+ extend Macros
9
15
 
10
- require_relative 'jimmy/domain'
11
- require_relative 'jimmy/schema'
12
- require_relative 'jimmy/reference'
13
- require_relative 'jimmy/type_reference'
14
- require_relative 'jimmy/schema_creation'
15
- require_relative 'jimmy/schema_types'
16
- require_relative 'jimmy/schema_type'
17
- require_relative 'jimmy/combination'
18
- require_relative 'jimmy/link'
19
- require_relative 'jimmy/validation_error'
20
- require_relative 'jimmy/definitions'
16
+ # @see SchemerFactory#initialize
17
+ def self.schemer(*args, **opts)
18
+ SchemerFactory.new(*args, **opts).schemer
19
+ end
20
+
21
+ # Passes +schema+ to {Schema.new}, unless it is already a {Schema}, in which
22
+ # case it is returned unmodified.
23
+ # @param [Schema, Object] schema
24
+ # @return [Schema]
25
+ def self.Schema(schema) # rubocop:disable Naming/MethodName
26
+ schema.is_a?(Schema) ? schema : Schema.new(schema)
27
+ end
28
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jimmy/declaration/composites'
4
+ require 'jimmy/declaration/number'
5
+ require 'jimmy/declaration/object'
6
+ require 'jimmy/declaration/string'
7
+ require 'jimmy/declaration/types'
8
+
9
+ require 'jimmy/declaration/assertion'
10
+ require 'jimmy/declaration/casting'
11
+
12
+ module Jimmy
13
+ # Contains methods for declaring or modifying schemas.
14
+ module Declaration
15
+ # Set the title of the schema.
16
+ # @param [String] title The title of the schema.
17
+ # @return [self] self, for chaining
18
+ def title(title)
19
+ assert_string title
20
+ set title: title
21
+ end
22
+
23
+ # Set the description of the schema.
24
+ # @param [String] description The description of the schema.
25
+ # @return [self] self, for chaining
26
+ def description(description)
27
+ assert_string description
28
+ set description: description
29
+ end
30
+
31
+ # Set the default value for the schema.
32
+ # @param [Object] default The default value for the schema.
33
+ # @return [self] self, for chaining
34
+ def default(default)
35
+ set default: default
36
+ end
37
+
38
+ # Set whether the schema is read-only.
39
+ # @param [true, false] is_read_only
40
+ # @return [self] self, for chaining
41
+ def read_only(is_read_only = true)
42
+ assert_boolean is_read_only
43
+ set readOnly: is_read_only
44
+ end
45
+
46
+ # Set whether the schema is write-only.
47
+ # @param [true, false] is_write_only
48
+ # @return [self] self, for chaining
49
+ def write_only(is_write_only = true)
50
+ assert_boolean is_write_only
51
+ set writeOnly: is_write_only
52
+ end
53
+
54
+ # Set a constant value that will be expected to match exactly.
55
+ # @param [Object] constant_value The value that will be expected to match
56
+ # exactly.
57
+ # @return [self] self, for chaining
58
+ def const(constant_value)
59
+ set const: constant_value
60
+ end
61
+
62
+ # Set an enum value for the schema.
63
+ # @param [Array] allowed_values The allowed values in the enum.
64
+ # @return [self] self, for chaining
65
+ def enum(allowed_values)
66
+ assert_array allowed_values, minimum: 1, unique: true
67
+ set enum: allowed_values
68
+ end
69
+
70
+ # Add examples to the schema
71
+ # @param [Array] examples One or more examples to add to the schema.
72
+ # @return [self] self, for chaining
73
+ def examples(*examples)
74
+ getset('examples') { [] }.concat examples
75
+ self
76
+ end
77
+
78
+ alias example examples
79
+
80
+ # Add a schema to this schema's +definitions+ property.
81
+ # @param [String] name The name of the schema definition.
82
+ # @param [Jimmy::Schema] schema
83
+ # @yieldparam schema [Jimmy::Schema] The defined schema.
84
+ # @return [self] self, for chaining
85
+ def define(name, schema = Schema.new, &block)
86
+ return definitions name, &block if name.is_a? Hash
87
+
88
+ assign_to_schema_hash 'definitions', name, schema, &block
89
+ end
90
+
91
+ # Add definitions to the schema's +definitions+ property.
92
+ # @param [Hash{String => Jimmy::Schema, nil}] definitions Definitions to be
93
+ # added to the schema's +definitions+ property.
94
+ # @yieldparam name [String] The name of a definition that was given a nil
95
+ # schema.
96
+ # @yieldparam schema [Jimmy::Schema] A new schema created in place of a
97
+ # nil hash value.
98
+ # @return [self] self, for chaining
99
+ def definitions(definitions, &block)
100
+ batch_assign_to_schema_hash 'definitions', definitions, &block
101
+ end
102
+
103
+ # Define the schema that this schema must not match.
104
+ # @param schema [Jimmy::Schema] The schema that must not match.
105
+ # @return [self] self, for chaining
106
+ def not(schema)
107
+ # TODO: combine more nots into an anyOf
108
+ set not: cast_schema(schema)
109
+ end
110
+
111
+ private
112
+
113
+ def set(props)
114
+ s = schema
115
+ props.each { |k, v| s[k.to_s] = v }
116
+ s
117
+ end
118
+
119
+ def getset(name)
120
+ set name => yield unless key? name
121
+ get name
122
+ end
123
+
124
+ def assign_to_schema_hash(property_name, key, schema)
125
+ property_name = cast_key(property_name)
126
+ key = cast_key(key)
127
+ hash = getset(property_name) { {} }
128
+ assert !hash.key?(key) do
129
+ "Property '#{property_name}' already has a member '#{key}'"
130
+ end
131
+ schema = cast_schema(schema)
132
+ yield schema if block_given?
133
+ hash[key] = schema
134
+ self
135
+ end
136
+
137
+ def batch_assign_to_schema_hash(property_name, hash)
138
+ assert_hash hash
139
+ hash.each do |name, schema|
140
+ name = cast_key(name)
141
+ if schema.nil? && block_given?
142
+ schema = Schema.new
143
+ yield name, schema
144
+ end
145
+ assign_to_schema_hash property_name, name, schema
146
+ end
147
+ self
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jimmy/error'
4
+
5
+ module Jimmy
6
+ module Declaration
7
+ BOOLEANS = Set.new([true, false]).freeze
8
+
9
+ private
10
+
11
+ def assert(condition = false)
12
+ raise Error::InvalidSchemaPropertyValue, yield unless condition
13
+ end
14
+
15
+ def assert_numeric(value, minimum: -Float::INFINITY)
16
+ assert(value.is_a? Numeric) { "Expected #{value.class} to be numeric" }
17
+ assert(value >= minimum) { "Expected #{value} to be at least #{minimum}" }
18
+ end
19
+
20
+ def assert_string(value)
21
+ assert(value.is_a? String) { "Expected #{value.class} to be a string" }
22
+ end
23
+
24
+ def assert_simple_type(value)
25
+ assert_string value
26
+ assert SIMPLE_TYPES.include?(value) do
27
+ "Expected #{value.class} to be one of #{SIMPLE_TYPES.to_a.join ', '}"
28
+ end
29
+ end
30
+
31
+ def assert_boolean(value)
32
+ assert BOOLEANS.include? value do
33
+ "Expected #{value.class} to be boolean"
34
+ end
35
+ end
36
+
37
+ def assert_array(value, unique: false, minimum: 0)
38
+ assert(value.is_a? Array) { "Expected #{value.class} to be an array" }
39
+ assert(value.uniq == value) { 'Expected a unique array' } if unique
40
+ assert value.length >= minimum do
41
+ "Expected an array of at least #{minimum} item(s)"
42
+ end
43
+ end
44
+
45
+ def assert_hash(value)
46
+ assert(value.is_a? Hash) { "Expected #{value.class} to be a hash" }
47
+ end
48
+
49
+ def assert_range(value)
50
+ assert(value.is_a? Range) { "Expected #{value.class} to be a range " }
51
+ end
52
+
53
+ def assert_regexp(value)
54
+ assert value.is_a? Regexp do
55
+ "Expected #{value.class} to be regular expression"
56
+ end
57
+ assert value.options.zero? do
58
+ "Expected #{value.inspect} not to have any options"
59
+ end
60
+ end
61
+
62
+ def valid_for(*types)
63
+ assert type? *types do
64
+ "The property is only valid for #{types.join ', '} schemas"
65
+ end
66
+ end
67
+
68
+ # Returns true if one of the given types is an existing type.
69
+ # @param [Array<String>] types The type or types to check.
70
+ # @return [true, false]
71
+ def type?(*types)
72
+ types.each &method(:assert_simple_type)
73
+ existing = get('type', nil)
74
+ if existing.is_a? Json::Array
75
+ (existing.to_a & types).any?
76
+ else
77
+ types.include? existing
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jimmy
4
+ module Declaration
5
+ private
6
+
7
+ CASTS = {
8
+ TrueClass => ->(s, _) { s },
9
+ FalseClass => ->(s, _) { s.nothing },
10
+ Regexp => ->(s, v) { s.string.pattern v },
11
+ Range => ->(s, v) { s.number.range v }
12
+ }.freeze
13
+
14
+ CASTABLE_CLASSES = CASTS.keys.freeze
15
+
16
+ # Cast the given value to a usable schema.
17
+ # @param [Object] value
18
+ # @return [Jimmy::Schema]
19
+ def cast_schema(value)
20
+ case value
21
+ when *CASTABLE_CLASSES then apply_cast(Schema.new, value)
22
+ when Schema then value
23
+ else
24
+ assert { "Expected #{value.class} to be a schema" }
25
+ end
26
+ end
27
+
28
+ def apply_cast(schema, value)
29
+ CASTS.each do |klass, proc|
30
+ return proc.call schema, value if value.is_a? klass
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jimmy
4
+ module Declaration
5
+ # Set the +anyOf+ value for the schema.
6
+ # @param [Array<Jimmy::Schema>] schemas The schemas to set as the value of
7
+ # +anyOf+.
8
+ # @return [self] self, for chaining
9
+ def any_of(*schemas)
10
+ set_composite 'anyOf', schemas.flatten
11
+ end
12
+
13
+ # Set the +allOf+ value for the schema.
14
+ # @param [Array<Jimmy::Schema>] schemas The schemas to set as the value of
15
+ # +allOf+.
16
+ # @return [self] self, for chaining
17
+ def all_of(*schemas)
18
+ set_composite 'allOf', schemas.flatten
19
+ end
20
+
21
+ # Set the +oneOf+ value for the schema.
22
+ # @param [Array<Jimmy::Schema>] schemas The schemas to set as the value of
23
+ # +oneOf+.
24
+ # @return [self] self, for chaining
25
+ def one_of(*schemas)
26
+ set_composite 'oneOf', schemas.flatten
27
+ end
28
+
29
+ private
30
+
31
+ # @return [self]
32
+ def set_composite(name, schemas)
33
+ assert_array schemas, minimum: 1
34
+ schemas = schemas.map(&method(:cast_schema))
35
+ assert schemas.none? { |s| s.anything? || s.nothing? } do
36
+ 'Absolutes make no sense in composites'
37
+ end
38
+ set name => schemas
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jimmy
4
+ module Declaration
5
+ # Set the number of which the value should be a multiple.
6
+ # @param [Numeric] number The number to set as the multipleOf value
7
+ # @return [self] self, for chaining
8
+ def multiple_of(number)
9
+ valid_for 'number', 'integer'
10
+ assert_numeric number
11
+ assert(number.positive?) { "Expected #{number} to be positive" }
12
+ set multipleOf: number
13
+ end
14
+
15
+ # Set minimum and maximum by providing a range.
16
+ # @param [Range] range The range to use for minimum and maximum values.
17
+ # @return [self] self, for chaining
18
+ def range(range)
19
+ assert_range range
20
+ schema.minimum range.begin
21
+ unless range.end.nil?
22
+ schema.maximum range.end, exclusive: range.exclude_end?
23
+ end
24
+ self
25
+ end
26
+ end
27
+ end