jimmy 0.5.5
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.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.ruby-version +1 -0
- data/Gemfile +3 -0
- data/LICENSE +13 -0
- data/README.md +151 -0
- data/Rakefile +4 -0
- data/circle.yml +11 -0
- data/jimmy.gemspec +28 -0
- data/lib/jimmy.rb +20 -0
- data/lib/jimmy/combination.rb +34 -0
- data/lib/jimmy/definitions.rb +38 -0
- data/lib/jimmy/domain.rb +111 -0
- data/lib/jimmy/link.rb +93 -0
- data/lib/jimmy/reference.rb +39 -0
- data/lib/jimmy/schema.rb +114 -0
- data/lib/jimmy/schema_creation.rb +121 -0
- data/lib/jimmy/schema_type.rb +100 -0
- data/lib/jimmy/schema_types.rb +42 -0
- data/lib/jimmy/schema_types/array.rb +30 -0
- data/lib/jimmy/schema_types/boolean.rb +6 -0
- data/lib/jimmy/schema_types/integer.rb +8 -0
- data/lib/jimmy/schema_types/null.rb +6 -0
- data/lib/jimmy/schema_types/number.rb +34 -0
- data/lib/jimmy/schema_types/object.rb +45 -0
- data/lib/jimmy/schema_types/string.rb +40 -0
- data/lib/jimmy/symbol_array.rb +17 -0
- data/lib/jimmy/transform_keys.rb +39 -0
- data/lib/jimmy/type_reference.rb +14 -0
- data/lib/jimmy/validation_error.rb +20 -0
- data/lib/jimmy/version.rb +3 -0
- metadata +144 -0
data/lib/jimmy/link.rb
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Jimmy
|
4
|
+
class Link < Hash
|
5
|
+
attr_reader :schema
|
6
|
+
|
7
|
+
def initialize(schema, rel, href)
|
8
|
+
@schema = schema
|
9
|
+
merge! 'rel' => rel.to_s,
|
10
|
+
'href' => href.to_s
|
11
|
+
end
|
12
|
+
|
13
|
+
def dsl
|
14
|
+
@dsl ||= DSL.new(self)
|
15
|
+
end
|
16
|
+
|
17
|
+
def schemas
|
18
|
+
@schemas ||= {}
|
19
|
+
end
|
20
|
+
|
21
|
+
def domain
|
22
|
+
schema.domain
|
23
|
+
end
|
24
|
+
|
25
|
+
def compile
|
26
|
+
merge schemas.map { |k, v| [(k ? "#{k}Schema" : 'schema'), v.compile] }.to_h
|
27
|
+
end
|
28
|
+
|
29
|
+
def schema_creator
|
30
|
+
@schema_creator ||= SchemaCreator.new(self)
|
31
|
+
end
|
32
|
+
|
33
|
+
class SchemaCreator < Hash
|
34
|
+
include SchemaCreation::Referencing
|
35
|
+
extend Forwardable
|
36
|
+
delegate [:schema, :domain] => :@link
|
37
|
+
|
38
|
+
def initialize(link)
|
39
|
+
@link = link
|
40
|
+
end
|
41
|
+
|
42
|
+
def parent
|
43
|
+
schema
|
44
|
+
end
|
45
|
+
|
46
|
+
SchemaCreation.apply_to(self) { |schema, prefix| @link.schemas[prefix] = schema }
|
47
|
+
end
|
48
|
+
|
49
|
+
class DSL
|
50
|
+
attr_reader :link
|
51
|
+
|
52
|
+
def initialize(link)
|
53
|
+
@link = link
|
54
|
+
end
|
55
|
+
|
56
|
+
def domain
|
57
|
+
link.domain
|
58
|
+
end
|
59
|
+
|
60
|
+
def title(value)
|
61
|
+
link['title'] = value
|
62
|
+
end
|
63
|
+
|
64
|
+
def method(value)
|
65
|
+
link['method'] = value.to_s.upcase
|
66
|
+
end
|
67
|
+
|
68
|
+
def evaluate(&block)
|
69
|
+
instance_exec &block
|
70
|
+
end
|
71
|
+
|
72
|
+
def schema(*args, prefix: nil, **opts, &block)
|
73
|
+
if args.empty? && opts.any?
|
74
|
+
args = opts.shift
|
75
|
+
type = args.shift
|
76
|
+
else
|
77
|
+
type = args.shift || :object
|
78
|
+
end
|
79
|
+
args.unshift type, prefix
|
80
|
+
args << opts if opts.any?
|
81
|
+
link.schema_creator.__send__ *args, &block
|
82
|
+
end
|
83
|
+
|
84
|
+
def target_schema(*args, **opts, &block)
|
85
|
+
schema *args, **opts.merge(prefix: :target), &block
|
86
|
+
end
|
87
|
+
|
88
|
+
def set(**values)
|
89
|
+
values.each { |k, v| link[k.to_s] = v }
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require_relative './transform_keys'
|
2
|
+
|
3
|
+
module Jimmy
|
4
|
+
class Reference
|
5
|
+
include SchemaCreation::MetadataMethods
|
6
|
+
attr_reader :uri, :data
|
7
|
+
|
8
|
+
def initialize(uri, domain, nullable = false, *args, **opts, &block)
|
9
|
+
@uri = TransformKeys.transformer.transform_ref(uri, domain.options[:transform_keys])
|
10
|
+
@nullable = nullable
|
11
|
+
@data = {}
|
12
|
+
args.each { |arg| __send__ arg }
|
13
|
+
opts.each { |arg| __send__ *arg }
|
14
|
+
instance_exec &block if block
|
15
|
+
end
|
16
|
+
|
17
|
+
def compile
|
18
|
+
data.merge(nullable? ?
|
19
|
+
{
|
20
|
+
'anyOf' => [
|
21
|
+
{'type' => 'null'},
|
22
|
+
ref_hash
|
23
|
+
]
|
24
|
+
} :
|
25
|
+
ref_hash
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
def nullable?
|
30
|
+
@nullable
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def ref_hash
|
36
|
+
{'$ref' => uri}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/jimmy/schema.rb
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
module Jimmy
|
2
|
+
class Schema
|
3
|
+
JSON_SCHEMA_URI = 'http://json-schema.org/draft-04/schema#'
|
4
|
+
JSON_HYPER_SCHEMA_URI = 'http://json-schema.org/draft-04/hyper-schema#'
|
5
|
+
|
6
|
+
attr_reader :dsl, :attrs, :domain, :type, :parent
|
7
|
+
attr_writer :name
|
8
|
+
attr_accessor :nullable
|
9
|
+
|
10
|
+
@argument_handlers = Hash.new { |hash, key| hash[key] = {} }
|
11
|
+
|
12
|
+
def self.set_argument_handler(schema_class, arg_class, handler)
|
13
|
+
@argument_handlers[schema_class][arg_class] = handler
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.argument_hander(schema_class, argument)
|
17
|
+
handlers = {}
|
18
|
+
until schema_class == SchemaType do
|
19
|
+
handlers = (@argument_handlers[schema_class] || {}).merge(handlers)
|
20
|
+
schema_class = schema_class.superclass
|
21
|
+
end
|
22
|
+
result = handlers.find { |k, _| argument.is_a? k }
|
23
|
+
result && result.last
|
24
|
+
end
|
25
|
+
|
26
|
+
def compile
|
27
|
+
compiler = nil
|
28
|
+
schema_class = SchemaTypes[type]
|
29
|
+
until schema_class == SchemaType do
|
30
|
+
compiler ||= SchemaTypes.compilers[schema_class]
|
31
|
+
schema_class = schema_class.superclass
|
32
|
+
end
|
33
|
+
hash = {}
|
34
|
+
hash['type'] = nullable ? ['null', type.to_s] : type.to_s
|
35
|
+
hash['definitions'] = definitions.compile unless definitions.empty?
|
36
|
+
hash['links'] = links.map &:compile unless links.empty?
|
37
|
+
hash.merge! data
|
38
|
+
dsl.evaluate compiler, hash if compiler
|
39
|
+
hash['enum'] |= [nil] if nullable && hash.key?('enum')
|
40
|
+
hash
|
41
|
+
end
|
42
|
+
|
43
|
+
def name
|
44
|
+
@name || (parent && parent.name)
|
45
|
+
end
|
46
|
+
|
47
|
+
def uri
|
48
|
+
domain.uri_for name
|
49
|
+
end
|
50
|
+
|
51
|
+
def definitions
|
52
|
+
@definitions ||= Definitions.new(self)
|
53
|
+
end
|
54
|
+
|
55
|
+
def links
|
56
|
+
@links ||= []
|
57
|
+
end
|
58
|
+
|
59
|
+
def data
|
60
|
+
@data ||= {}
|
61
|
+
end
|
62
|
+
|
63
|
+
def hyper?
|
64
|
+
links.any?
|
65
|
+
end
|
66
|
+
|
67
|
+
def schema_uri
|
68
|
+
hyper? ? JSON_HYPER_SCHEMA_URI : JSON_SCHEMA_URI
|
69
|
+
end
|
70
|
+
|
71
|
+
def to_h
|
72
|
+
{'$schema' => schema_uri}.tap do |h|
|
73
|
+
h['id'] = uri.to_s if name
|
74
|
+
h.merge! compile
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def validate(data)
|
79
|
+
errors = JSON::Validator.fully_validate(JSON::Validator.schema_for_uri(uri).schema, data, errors_as_objects: true)
|
80
|
+
raise ValidationError.new(self, data, errors) unless errors.empty?
|
81
|
+
end
|
82
|
+
|
83
|
+
def initialize(type, parent)
|
84
|
+
@attrs = {}
|
85
|
+
@type = type
|
86
|
+
@domain = parent.domain
|
87
|
+
@dsl = SchemaTypes.dsls[type].new(self)
|
88
|
+
@parent = parent if parent.is_a? self.class
|
89
|
+
end
|
90
|
+
|
91
|
+
def setup(*args, **locals, &block)
|
92
|
+
args.each do |arg|
|
93
|
+
case arg
|
94
|
+
when Symbol
|
95
|
+
dsl.__send__ arg
|
96
|
+
when Hash
|
97
|
+
arg.each { |k, v| dsl.__send__ k, v }
|
98
|
+
else
|
99
|
+
handler = Schema.argument_hander(SchemaTypes[type], arg)
|
100
|
+
raise "`#{type}` cannot handle arguments of type #{arg.class.name}" unless handler
|
101
|
+
dsl.evaluate handler, arg
|
102
|
+
end
|
103
|
+
end
|
104
|
+
if block
|
105
|
+
if dsl.respond_to? :with_locals
|
106
|
+
dsl.with_locals(locals) { dsl.evaluate block }
|
107
|
+
else
|
108
|
+
dsl.evaluate block
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
module Jimmy
|
2
|
+
class SchemaCreation
|
3
|
+
|
4
|
+
@handlers = {}
|
5
|
+
|
6
|
+
class << self
|
7
|
+
|
8
|
+
attr_reader :handlers
|
9
|
+
|
10
|
+
def apply_to(klass, &handler)
|
11
|
+
@handlers[klass] = handler
|
12
|
+
%i(one all any).each do |condition|
|
13
|
+
klass.__send__ :define_method, :"#{condition}_of" do |*args, &inner_block|
|
14
|
+
Combination.new(condition, schema).tap do |combo|
|
15
|
+
combo.with_locals(locals) { combo.evaluate inner_block }
|
16
|
+
instance_exec combo, *args, &handler
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
klass.include DefiningMethods
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module MetadataMethods
|
25
|
+
def set(**values)
|
26
|
+
values.each { |k, v| data[k.to_s] = v }
|
27
|
+
end
|
28
|
+
|
29
|
+
%i[title description default].each { |k| define_method(k) { |v| set k => v } }
|
30
|
+
end
|
31
|
+
|
32
|
+
module Referencing
|
33
|
+
def method_missing(name, *args, &block)
|
34
|
+
if schema.definitions[name]
|
35
|
+
ref *args, definition(name), &block
|
36
|
+
else
|
37
|
+
super
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def respond_to_missing?(name, *)
|
42
|
+
schema.definitions.key?(name) || super
|
43
|
+
end
|
44
|
+
|
45
|
+
def definition(id)
|
46
|
+
"/#{schema.name}#/definitions/#{id}"
|
47
|
+
end
|
48
|
+
|
49
|
+
def ref(*args, uri, &block)
|
50
|
+
handler = SchemaCreation.handlers[self.class]
|
51
|
+
instance_exec(Reference.new(uri, domain, args.delete(:nullable), &block), *args, &handler) if handler
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
module DefiningMethods
|
56
|
+
include MetadataMethods
|
57
|
+
|
58
|
+
def locals
|
59
|
+
@locals ||= {}
|
60
|
+
end
|
61
|
+
|
62
|
+
def with_locals(**locals)
|
63
|
+
locals.each_key do |key|
|
64
|
+
raise "Local '#{key}' conflicts with an existing DSL method" if reserved? key
|
65
|
+
end
|
66
|
+
original = locals
|
67
|
+
@locals = original.merge(locals)
|
68
|
+
yield.tap { @locals = original }
|
69
|
+
end
|
70
|
+
|
71
|
+
def respond_to_missing?(method, *)
|
72
|
+
locals.key?(method) || reserved?(method, false) || super
|
73
|
+
end
|
74
|
+
|
75
|
+
def method_missing(method, *args, &block)
|
76
|
+
return locals[method] if locals.key?(method)
|
77
|
+
|
78
|
+
if SchemaTypes.key? method
|
79
|
+
handler = SchemaCreation.handlers[self.class]
|
80
|
+
self.class.__send__ :define_method, method do |*inner_args, &inner_block|
|
81
|
+
handler_args = handler && inner_args.shift(handler.arity - 1)
|
82
|
+
child_schema = Schema.new(
|
83
|
+
method,
|
84
|
+
respond_to?(:schema) ? schema : domain
|
85
|
+
)
|
86
|
+
child_schema.name = @schema_name if is_a? Domain
|
87
|
+
child_schema.setup *inner_args, **locals, &inner_block
|
88
|
+
instance_exec child_schema, *handler_args, &handler if handler
|
89
|
+
child_schema.dsl
|
90
|
+
end
|
91
|
+
return __send__ method, *args, &block
|
92
|
+
end
|
93
|
+
|
94
|
+
domain.autoload_type method
|
95
|
+
|
96
|
+
if domain.types.key? method
|
97
|
+
return instance_exec TypeReference.new(method, domain, args.include?(:nullable)), *args, &SchemaCreation.handlers[self.class]
|
98
|
+
end
|
99
|
+
|
100
|
+
if kind_of_array?(method)
|
101
|
+
return array(*args) { __send__ method[0..-7], &block }
|
102
|
+
end
|
103
|
+
|
104
|
+
super method, *args, &block
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def reserved?(key, all = true)
|
110
|
+
domain.autoload_type key
|
111
|
+
(all && respond_to?(key)) || SchemaTypes.key?(key) || domain.types.key?(key) || kind_of_array?(key)
|
112
|
+
end
|
113
|
+
|
114
|
+
def kind_of_array?(key)
|
115
|
+
key =~ /^(\w+)_array$/ && reserved?($1.to_sym)
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
require_relative 'schema'
|
4
|
+
|
5
|
+
module Jimmy
|
6
|
+
class SchemaType
|
7
|
+
|
8
|
+
class << self
|
9
|
+
|
10
|
+
def register!
|
11
|
+
SchemaTypes.register self
|
12
|
+
end
|
13
|
+
|
14
|
+
def trait(*args, &handler)
|
15
|
+
args.each do |name_or_type|
|
16
|
+
case name_or_type
|
17
|
+
when Symbol
|
18
|
+
handler ||= proc { |value| attrs[name_or_type] = value }
|
19
|
+
self::DSL.__send__ :define_method, name_or_type, handler
|
20
|
+
when Class
|
21
|
+
Schema.set_argument_handler self, name_or_type, handler
|
22
|
+
else
|
23
|
+
raise 'Trait must be a Symbol or a Class'
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def nested(&handler)
|
29
|
+
SchemaTypes.nested_handlers[self] = handler
|
30
|
+
end
|
31
|
+
|
32
|
+
def compile(&handler)
|
33
|
+
SchemaTypes.compilers[self] = handler
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
class DSL
|
39
|
+
extend Forwardable
|
40
|
+
include SchemaCreation::Referencing
|
41
|
+
include SchemaCreation::MetadataMethods
|
42
|
+
|
43
|
+
attr_reader :schema
|
44
|
+
|
45
|
+
delegate %i(attrs domain) => :schema
|
46
|
+
|
47
|
+
def initialize(schema)
|
48
|
+
@schema = schema
|
49
|
+
end
|
50
|
+
|
51
|
+
def evaluate(proc, *args)
|
52
|
+
instance_exec *args, &proc
|
53
|
+
end
|
54
|
+
|
55
|
+
def camelize_attrs(*args)
|
56
|
+
included_args = args.flatten.reject { |arg| attrs[arg].nil? }
|
57
|
+
included_args.map { |arg| [arg.to_s.gsub(/_([a-z])/) { $1.upcase }, attrs[arg]] }.to_h
|
58
|
+
end
|
59
|
+
|
60
|
+
def include(*partial_names, **locals)
|
61
|
+
partial_names.each do |name|
|
62
|
+
with_locals locals do
|
63
|
+
evaluate_partial domain.partials[name.to_s]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def definitions(&block)
|
69
|
+
schema.definitions.evaluate &block
|
70
|
+
end
|
71
|
+
|
72
|
+
def define(type, *args, &block)
|
73
|
+
definitions { __send__ type, *args, &block }
|
74
|
+
end
|
75
|
+
|
76
|
+
def data
|
77
|
+
schema.data
|
78
|
+
end
|
79
|
+
|
80
|
+
def link(rel_and_href, &block)
|
81
|
+
link = Link.new(schema, *rel_and_href.first)
|
82
|
+
schema.links << link
|
83
|
+
link.dsl.evaluate &block if block
|
84
|
+
end
|
85
|
+
|
86
|
+
def nullable
|
87
|
+
schema.nullable = true
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
# Minimize collisions with local scope (hence the weird name __args)
|
93
|
+
def evaluate_partial(__args)
|
94
|
+
instance_eval *__args
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
end
|