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