jimmy 0.5.5
Sign up to get free protection for your applications and to get access to all the features.
- 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
|