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.
@@ -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