jimmy 0.5.5

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