openapi_blocks 0.3.1 → 0.5.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.
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenapiBlocks
4
+ module Serialization # rubocop:disable Style/Documentation
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ end
8
+
9
+ module ClassMethods # rubocop:disable Metrics/ModuleLength,Style/Documentation
10
+ def serialize(resource, instance: nil) # rubocop:disable Lint/UnusedMethodArgument
11
+ extractor = compiled_extractor
12
+ if resource.respond_to?(:each)
13
+ resource.map { |r| extractor.call(r) }
14
+ else
15
+ extractor.call(resource)
16
+ end
17
+ end
18
+
19
+ def to_json(resource)
20
+ Oj.dump(serialize(resource), mode: :compat)
21
+ end
22
+
23
+ def fields
24
+ @fields ||= resolve_fields
25
+ end
26
+
27
+ def compiled_extractor
28
+ @compiled_extractor ||= build_compiled_extractor
29
+ end
30
+
31
+ private
32
+
33
+ def build_compiled_extractor # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
34
+ classified = classify_fields
35
+
36
+ model_lines = classified[:model].map { |f| %("#{f}" => object.public_send("#{f}")) }
37
+ virtual_lines = classified[:virtual].map { |f| %("#{f}" => inst.public_send("#{f}")) }
38
+ delegated_lines = classified[:delegated].map { |f| %("#{f}" => object.public_send("#{f}")) }
39
+ assoc_lines = classified[:association].map do |f|
40
+ %("#{f}" => _serialize_assoc_#{f}(object))
41
+ end
42
+
43
+ classified[:association].each { |field| build_assoc_method(field) }
44
+
45
+ all_lines = (model_lines + delegated_lines + virtual_lines + assoc_lines).join(",\n ")
46
+
47
+ if classified[:virtual].any?
48
+ serializer_klass = self
49
+ class_eval(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition
50
+ def self._extract(object)
51
+ inst = #{serializer_klass}.new(object)
52
+ {
53
+ #{all_lines}
54
+ }
55
+ end
56
+ RUBY
57
+ else
58
+ class_eval(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition
59
+ def self._extract(object)
60
+ {
61
+ #{all_lines}
62
+ }
63
+ end
64
+ RUBY
65
+ end
66
+
67
+ method(:_extract)
68
+ end
69
+
70
+ def build_assoc_method(field) # rubocop:disable Metrics/MethodLength
71
+ assoc = assoc_metadata_by_name[field]
72
+ assoc_name = assoc[:name]
73
+ serializer = resolve_assoc_serializer(assoc_name)
74
+
75
+ if serializer.nil?
76
+ class_eval(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition
77
+ def self._serialize_assoc_#{field}(object)
78
+ object.public_send(:#{assoc_name}).as_json
79
+ end
80
+ RUBY
81
+ return
82
+ end
83
+
84
+ if assoc[:type] == :array
85
+ class_eval(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition
86
+ def self._serialize_assoc_#{field}(object)
87
+ val = object.public_send(:#{assoc_name})
88
+ return nil if val.nil?
89
+ val.map { |v| #{serializer}.serialize(v) }
90
+ end
91
+ RUBY
92
+ else
93
+ class_eval(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition
94
+ def self._serialize_assoc_#{field}(object)
95
+ val = object.public_send(:#{assoc_name})
96
+ val.nil? ? nil : #{serializer}.serialize(val)
97
+ end
98
+ RUBY
99
+ end
100
+ end
101
+
102
+ def classify_fields # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
103
+ model_set = Set.new(resolve_model_fields)
104
+ assoc_set = Set.new(assoc_metadata_by_name.keys)
105
+ virtual_set = Set.new(
106
+ Array(_virtual_attributes).map { |a| a[:name].to_s }
107
+ )
108
+
109
+ result = { model: [], virtual: [], delegated: [], association: [] }
110
+ fields.each do |field|
111
+ result[if assoc_set.include?(field)
112
+ :association
113
+ elsif model_set.include?(field)
114
+ :model
115
+ elsif virtual_set.include?(field) && method_defined?(field.to_sym)
116
+ :virtual # método definido no resource
117
+ elsif virtual_set.include?(field)
118
+ :delegated # método definido no model
119
+ else # rubocop:disable Lint/DuplicateBranch
120
+ :delegated
121
+ end] << field
122
+ end
123
+ result
124
+ end
125
+
126
+ def assoc_metadata_by_name
127
+ @assoc_metadata_by_name ||= begin
128
+ klass = self
129
+ result = {}
130
+ while klass && klass != serialization_sentinel
131
+ Array(klass._associations)
132
+ .each { |a| result[a[:name].to_s] ||= a }
133
+ klass = klass.superclass
134
+ end
135
+ result
136
+ end
137
+ end
138
+
139
+ def resolve_fields # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
140
+ klass = self
141
+ sentinel = serialization_sentinel
142
+
143
+ model_fields = resolve_model_fields
144
+ virtual_fields = []
145
+ ignored_fields = []
146
+ assoc_fields = []
147
+
148
+ while klass && klass != sentinel
149
+ virtual_fields += Array(klass._virtual_attributes).map { |a| a[:name].to_s }
150
+ ignored_fields += Array(klass._ignored)
151
+ assoc_fields += Array(klass._associations).map { |a| a[:name].to_s }
152
+ klass = klass.superclass
153
+ end
154
+
155
+ (model_fields + virtual_fields + assoc_fields - ignored_fields).uniq
156
+ end
157
+
158
+ def resolve_model_fields
159
+ klass = self
160
+ sentinel = serialization_sentinel
161
+ while klass && klass != sentinel
162
+ begin
163
+ return klass.model.column_names
164
+ rescue StandardError
165
+ klass = klass.superclass
166
+ end
167
+ end
168
+ []
169
+ end
170
+
171
+ def serialization_sentinel
172
+ raise NotImplementedError
173
+ end
174
+
175
+ def resolve_assoc_serializer(assoc_name)
176
+ classified = assoc_name.to_s.classify
177
+
178
+ ["#{classified}Serializer", "#{classified}Openapi"].each do |name|
179
+ klass = Object.const_get(name)
180
+ return klass._resource if klass.respond_to?(:_resource) && klass._resource
181
+ return klass if klass.respond_to?(:serialize)
182
+ rescue NameError
183
+ next
184
+ end
185
+
186
+ nil
187
+ end
188
+ end
189
+
190
+ attr_reader :object, :parent
191
+
192
+ def initialize(object, parent = nil)
193
+ @object = object
194
+ @parent = parent
195
+ end
196
+ end
197
+ end
@@ -1,193 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenapiBlocks
4
- module Serializer # rubocop:disable Style/Documentation
5
- def self.included(base)
6
- base.extend(ClassMethods)
7
- end
8
-
9
- module ClassMethods # rubocop:disable Metrics/ModuleLength,Style/Documentation
10
- def serialize(resource, instance: nil) # rubocop:disable Lint/UnusedMethodArgument
11
- extractor = compiled_extractor
12
- if resource.respond_to?(:each)
13
- resource.map { |r| extractor.call(r) }
14
- else
15
- extractor.call(resource)
16
- end
17
- end
18
-
19
- def to_json(resource)
20
- Oj.dump(serialize(resource), mode: :compat)
21
- end
22
-
23
- def fields
24
- @fields ||= resolve_fields
25
- end
26
-
27
- def compiled_extractor
28
- @compiled_extractor ||= build_compiled_extractor
29
- end
4
+ class Serializer # rubocop:disable Style/Documentation
5
+ include Concerns::Schemable
6
+ include Serialization
30
7
 
8
+ class << self
31
9
  private
32
10
 
33
- def build_compiled_extractor # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
34
- classified = classify_fields
35
-
36
- model_lines = classified[:model].map { |f| %("#{f}" => object.public_send(:#{f})) }
37
- virtual_lines = classified[:virtual].map { |f| %("#{f}" => inst.public_send(:#{f})) }
38
- delegated_lines = classified[:delegated].map { |f| %("#{f}" => object.public_send(:#{f})) }
39
- assoc_lines = classified[:association].map do |f|
40
- %("#{f}" => _serialize_assoc_#{f}(object))
41
- end
42
-
43
- classified[:association].each { |field| build_assoc_method(field) }
44
-
45
- all_lines = (model_lines + delegated_lines + virtual_lines + assoc_lines).join(",\n ")
46
-
47
- if classified[:virtual].any?
48
- serializer_klass = self
49
- class_eval(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition
50
- def self._extract(object)
51
- inst = #{serializer_klass}.new(object)
52
- {
53
- #{all_lines}
54
- }
55
- end
56
- RUBY
57
- else
58
- class_eval(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition
59
- def self._extract(object)
60
- {
61
- #{all_lines}
62
- }
63
- end
64
- RUBY
65
- end
66
-
67
- method(:_extract)
68
- end
69
-
70
- def build_assoc_method(field) # rubocop:disable Metrics/MethodLength
71
- assoc = assoc_metadata_by_name[field]
72
- assoc_name = assoc[:name]
73
- serializer = resolve_assoc_serializer(assoc_name)
74
-
75
- if serializer.nil?
76
- class_eval(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition
77
- def self._serialize_assoc_#{field}(object)
78
- object.public_send(:#{assoc_name}).as_json
79
- end
80
- RUBY
81
- return
82
- end
83
-
84
- if assoc[:type] == :array
85
- class_eval(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition
86
- def self._serialize_assoc_#{field}(object)
87
- val = object.public_send(:#{assoc_name})
88
- return nil if val.nil?
89
- val.map { |v| #{serializer}.serialize(v) }
90
- end
91
- RUBY
92
- else
93
- class_eval(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition
94
- def self._serialize_assoc_#{field}(object)
95
- val = object.public_send(:#{assoc_name})
96
- val.nil? ? nil : #{serializer}.serialize(val)
97
- end
98
- RUBY
99
- end
100
- end
101
-
102
- def resolve_assoc_serializer(assoc_name)
103
- classified = assoc_name.to_s.classify
104
-
105
- ["#{classified}Resource", "#{classified}Openapi"].each do |name|
106
- klass = Object.const_get(name)
107
- return klass._resource if klass.respond_to?(:_resource) && klass._resource
108
- return klass if klass.respond_to?(:serialize)
109
- rescue NameError
110
- next
111
- end
112
-
113
- nil
114
- end
115
-
116
- def classify_fields # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
117
- model_set = Set.new(resolve_model_fields)
118
- assoc_set = Set.new(assoc_metadata_by_name.keys)
119
- virtual_set = Set.new(
120
- Array(_virtual_attributes).map { |a| a[:name].to_s }
121
- )
122
-
123
- result = { model: [], virtual: [], delegated: [], association: [] }
124
- fields.each do |field|
125
- result[if assoc_set.include?(field)
126
- :association
127
- elsif model_set.include?(field)
128
- :model
129
- elsif virtual_set.include?(field) && method_defined?(field.to_sym)
130
- :virtual # método definido no resource
131
- elsif virtual_set.include?(field)
132
- :delegated # método definido no model
133
- else # rubocop:disable Lint/DuplicateBranch
134
- :delegated
135
- end] << field
136
- end
137
- result
138
- end
139
-
140
- def assoc_metadata_by_name
141
- @assoc_metadata_by_name ||= begin
142
- klass = self
143
- result = {}
144
- while klass && klass != OpenapiBlocks::Base
145
- Array(klass._associations)
146
- .each { |a| result[a[:name].to_s] ||= a }
147
- klass = klass.superclass
148
- end
149
- result
150
- end
151
- end
152
-
153
- def resolve_fields # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
154
- klass = self
155
-
156
- model_fields = resolve_model_fields
157
- virtual_fields = []
158
- ignored_fields = []
159
- assoc_fields = []
11
+ def serialization_sentinel = OpenapiBlocks::Serializer
160
12
 
161
- while klass && klass != OpenapiBlocks::Base
162
- virtual_fields += Array(klass._virtual_attributes)
163
- .map { |a| a[:name].to_s }
164
- ignored_fields += Array(klass._ignored)
165
- assoc_fields += Array(klass._associations)
166
- .map { |a| a[:name].to_s }
167
- klass = klass.superclass
168
- end
169
-
170
- (model_fields + virtual_fields + assoc_fields - ignored_fields).uniq
13
+ def infer_model
14
+ model_name = name
15
+ .gsub(/Serializer$/, "")
16
+ .split("::").last
17
+ Object.const_get(model_name)
18
+ rescue NameError
19
+ raise Error, "Could not infer model from #{name}. Use `model ModelClass` to define it explicitly."
171
20
  end
172
-
173
- def resolve_model_fields
174
- klass = self
175
- while klass && klass != OpenapiBlocks::Base
176
- begin
177
- return klass.model.column_names
178
- rescue StandardError
179
- klass = klass.superclass
180
- end
181
- end
182
- []
183
- end
184
- end
185
-
186
- attr_reader :object, :parent
187
-
188
- def initialize(object, parent = nil)
189
- @object = object
190
- @parent = parent
191
21
  end
192
22
  end
193
23
  end
@@ -16,15 +16,17 @@ module OpenapiBlocks
16
16
  schemas = @openapi_classes.each_with_object({}) do |klass, hash|
17
17
  schema_klass = klass.respond_to?(:_resource) && klass._resource ? klass._resource : klass
18
18
 
19
- begin
20
- next unless schema_klass.model
19
+ model = begin
20
+ schema_klass.model
21
21
  rescue StandardError
22
22
  next
23
23
  end
24
24
 
25
- schema_name = schema_klass.model.name
25
+ next unless model
26
+
27
+ schema_name = model.name
26
28
  extractor = Schema::Extractor.new(schema_klass)
27
- validator = Schema::Validator.new(schema_klass.model)
29
+ validator = Schema::Validator.new(model)
28
30
 
29
31
  schema = extractor.extract
30
32
  schema[:properties] = merge_validations(schema[:properties], validator.extract)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenapiBlocks
4
- VERSION = "0.3.1"
4
+ VERSION = "0.5.0"
5
5
  end
@@ -18,13 +18,17 @@ require_relative "openapi_blocks/spec/components"
18
18
  require_relative "openapi_blocks/spec/paths"
19
19
  require_relative "openapi_blocks/spec/document"
20
20
  require_relative "openapi_blocks/builder"
21
- require_relative "openapi_blocks/serializer"
22
- require_relative "openapi_blocks/base"
23
21
  require_relative "openapi_blocks/engine"
24
22
  require_relative "openapi_blocks/railtie"
25
23
  require_relative "openapi_blocks/operation_builder"
26
- require_relative "openapi_blocks/resource"
24
+ require_relative "openapi_blocks/concerns/schemable"
25
+ require_relative "openapi_blocks/concerns/documentable"
26
+ require_relative "openapi_blocks/serialization"
27
+ require_relative "openapi_blocks/base"
28
+ require_relative "openapi_blocks/serializer"
27
29
  require_relative "openapi_blocks/controller"
30
+ require_relative "openapi_blocks/registry"
31
+ require_relative "openapi_blocks/auto_serialize"
28
32
 
29
33
  module OpenapiBlocks # rubocop:disable Style/Documentation
30
34
  class Error < StandardError; end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openapi_blocks
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Caio Santos
@@ -122,7 +122,9 @@ dependencies:
122
122
  - !ruby/object:Gem::Version
123
123
  version: '2.1'
124
124
  description: Generates OpenAPI specs automatically from ActiveRecord models, ActiveModel
125
- validations and Rails routes, inspired by ActiveModel::Serializer.
125
+ validations and Rails routes. Includes a built-in serializer ~3.6x faster than as_json,
126
+ Scalar and Swagger UI out of the box, and optional auto-serialization via render
127
+ json:. Inspired by ActiveModel::Serializer.
126
128
  email:
127
129
  - caio.francelinosena@gmail.com
128
130
  executables: []
@@ -137,10 +139,19 @@ files:
137
139
  - Rakefile
138
140
  - app/controllers/openapi_blocks/spec_controller.rb
139
141
  - config/routes.rb
142
+ - lib/generators/openapi_blocks/install/install_generator.rb
143
+ - lib/generators/openapi_blocks/install/templates/initializer.rb.tt
144
+ - lib/generators/openapi_blocks/openapi/openapi_generator.rb
145
+ - lib/generators/openapi_blocks/openapi/templates/openapi.rb.tt
146
+ - lib/generators/openapi_blocks/serializer/serializer_generator.rb
147
+ - lib/generators/openapi_blocks/serializer/templates/serializer.rb.tt
140
148
  - lib/openapi_blocks.rb
149
+ - lib/openapi_blocks/auto_serialize.rb
141
150
  - lib/openapi_blocks/base.rb
142
151
  - lib/openapi_blocks/builder.rb
143
152
  - lib/openapi_blocks/cache.rb
153
+ - lib/openapi_blocks/concerns/documentable.rb
154
+ - lib/openapi_blocks/concerns/schemable.rb
144
155
  - lib/openapi_blocks/configuration.rb
145
156
  - lib/openapi_blocks/configuration/contact_builder.rb
146
157
  - lib/openapi_blocks/configuration/info_builder.rb
@@ -154,12 +165,13 @@ files:
154
165
  - lib/openapi_blocks/middleware.rb
155
166
  - lib/openapi_blocks/operation_builder.rb
156
167
  - lib/openapi_blocks/railtie.rb
157
- - lib/openapi_blocks/resource.rb
168
+ - lib/openapi_blocks/registry.rb
158
169
  - lib/openapi_blocks/routing/extractor.rb
159
170
  - lib/openapi_blocks/routing/operation.rb
160
171
  - lib/openapi_blocks/schema/extractor.rb
161
172
  - lib/openapi_blocks/schema/types.rb
162
173
  - lib/openapi_blocks/schema/validator.rb
174
+ - lib/openapi_blocks/serialization.rb
163
175
  - lib/openapi_blocks/serializer.rb
164
176
  - lib/openapi_blocks/spec/components.rb
165
177
  - lib/openapi_blocks/spec/document.rb
@@ -188,7 +200,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
188
200
  - !ruby/object:Gem::Version
189
201
  version: '0'
190
202
  requirements: []
191
- rubygems_version: 4.0.9
203
+ rubygems_version: 4.0.3
192
204
  specification_version: 4
193
- summary: DSL to generate OpenAPI 3.0/3.1 documentation for Rails applications
205
+ summary: OpenAPI 3.0/3.1 documentation and high-performance serializer for Rails
194
206
  test_files: []
@@ -1,20 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OpenapiBlocks
4
- class Resource < Base # rubocop:disable Style/Documentation
5
- class << self
6
- private
7
-
8
- def infer_model
9
- model_name = name
10
- .gsub(/Resource$/, "")
11
- .split("::")
12
- .last
13
-
14
- Object.const_get(model_name)
15
- rescue NameError
16
- raise Error, "Could not infer model from #{name}. Use `model ModelClass` to define it explicitly."
17
- end
18
- end
19
- end
20
- end