openapi_blocks 0.3.0 → 0.4.1
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 +4 -4
- data/CHANGELOG.md +46 -4
- data/README.md +100 -73
- data/README.pt-BR.md +147 -131
- data/app/controllers/openapi_blocks/spec_controller.rb +2 -2
- data/lib/openapi_blocks/auto_serialize.rb +54 -0
- data/lib/openapi_blocks/base.rb +6 -35
- data/lib/openapi_blocks/builder.rb +34 -2
- data/lib/openapi_blocks/concerns/documentable.rb +26 -0
- data/lib/openapi_blocks/concerns/schemable.rb +45 -0
- data/lib/openapi_blocks/configuration.rb +8 -1
- data/lib/openapi_blocks/controller.rb +9 -25
- data/lib/openapi_blocks/railtie.rb +10 -4
- data/lib/openapi_blocks/registry.rb +76 -0
- data/lib/openapi_blocks/routing/extractor.rb +16 -1
- data/lib/openapi_blocks/serialization.rb +197 -0
- data/lib/openapi_blocks/serializer.rb +12 -182
- data/lib/openapi_blocks/spec/components.rb +6 -4
- data/lib/openapi_blocks/version.rb +1 -1
- data/lib/openapi_blocks.rb +7 -3
- metadata +11 -5
- data/lib/openapi_blocks/resource.rb +0 -20
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenapiBlocks
|
|
4
|
+
module Concerns
|
|
5
|
+
module Documentable # rubocop:disable Style/Documentation
|
|
6
|
+
def self.included(base)
|
|
7
|
+
base.extend(ClassMethods)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
module ClassMethods # rubocop:disable Style/Documentation
|
|
11
|
+
attr_reader :_operations, :_tags
|
|
12
|
+
|
|
13
|
+
def operation(action, &block)
|
|
14
|
+
@_operations ||= {}
|
|
15
|
+
builder = OperationBuilder.new
|
|
16
|
+
builder.instance_eval(&block) if block
|
|
17
|
+
@_operations[action] = builder
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def tags(*values)
|
|
21
|
+
values.any? ? @_tags = values : @_tags
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenapiBlocks
|
|
4
|
+
module Concerns
|
|
5
|
+
module Schemable # rubocop:disable Style/Documentation
|
|
6
|
+
def self.included(base)
|
|
7
|
+
base.extend(ClassMethods)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
module ClassMethods # rubocop:disable Style/Documentation
|
|
11
|
+
attr_reader :_model, :_ignored, :_associations, :_virtual_attributes, :_serializes
|
|
12
|
+
|
|
13
|
+
def model(klass = nil)
|
|
14
|
+
klass ? @_model = klass : @_model ||= infer_model # rubocop:disable Naming/MemoizedInstanceVariableName
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def ignore(*attributes)
|
|
18
|
+
@_ignored ||= []
|
|
19
|
+
@_ignored.concat(attributes.map(&:to_s))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def association(name, type: nil, read_only: false)
|
|
23
|
+
@_associations ||= []
|
|
24
|
+
@_associations << { name: name, type: type, read_only: read_only }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def attribute(name, **)
|
|
28
|
+
@_virtual_attributes ||= []
|
|
29
|
+
@_virtual_attributes << { name: name, ** }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def serializes(*models)
|
|
33
|
+
@_serializes ||= []
|
|
34
|
+
@_serializes.concat(models)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def infer_model
|
|
40
|
+
raise NotImplementedError, "#{name} must implement infer_model"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -9,14 +9,20 @@ module OpenapiBlocks
|
|
|
9
9
|
SUPPORTED_VERSIONS = %w[3.1.0 3.0.3].freeze
|
|
10
10
|
|
|
11
11
|
attr_reader :openapi_version
|
|
12
|
-
attr_accessor :watch
|
|
12
|
+
attr_accessor :watch, :auto_serialize
|
|
13
13
|
|
|
14
14
|
def initialize
|
|
15
15
|
@openapi_version = "3.1.0"
|
|
16
16
|
@watch = :development
|
|
17
|
+
@auto_serialize = false
|
|
17
18
|
@info = InfoBuilder.new
|
|
18
19
|
@servers = []
|
|
19
20
|
@security = nil
|
|
21
|
+
@configured = false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def configured?
|
|
25
|
+
@configured
|
|
20
26
|
end
|
|
21
27
|
|
|
22
28
|
def openapi_version=(version)
|
|
@@ -29,6 +35,7 @@ module OpenapiBlocks
|
|
|
29
35
|
end
|
|
30
36
|
|
|
31
37
|
def info(&block)
|
|
38
|
+
@configured = true if block
|
|
32
39
|
@info.instance_eval(&block) if block
|
|
33
40
|
@info
|
|
34
41
|
end
|
|
@@ -2,39 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
module OpenapiBlocks
|
|
4
4
|
class Controller # rubocop:disable Style/Documentation
|
|
5
|
+
include Concerns::Documentable
|
|
6
|
+
|
|
5
7
|
class << self
|
|
6
|
-
attr_reader :_resource, :
|
|
8
|
+
attr_reader :_resource, :_controller_class
|
|
7
9
|
|
|
8
10
|
def resource(klass)
|
|
9
11
|
@_resource = klass
|
|
10
12
|
end
|
|
11
13
|
|
|
12
|
-
def
|
|
13
|
-
@
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def _associations
|
|
17
|
-
@_resource&._associations
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def _virtual_attributes
|
|
21
|
-
@_resource&._virtual_attributes
|
|
14
|
+
def controller(klass)
|
|
15
|
+
@_controller_class = klass
|
|
22
16
|
end
|
|
23
17
|
|
|
24
|
-
def
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def operation(action, &block)
|
|
29
|
-
@_operations ||= {}
|
|
30
|
-
builder = OperationBuilder.new
|
|
31
|
-
builder.instance_eval(&block) if block
|
|
32
|
-
@_operations[action] = builder
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def tags(*values)
|
|
36
|
-
values.any? ? @_tags = values : @_tags
|
|
37
|
-
end
|
|
18
|
+
def model = @_resource&.model
|
|
19
|
+
def _associations = @_resource&._associations
|
|
20
|
+
def _virtual_attributes = @_resource&._virtual_attributes
|
|
21
|
+
def _ignored = @_resource&._ignored
|
|
38
22
|
end
|
|
39
23
|
end
|
|
40
24
|
end
|
|
@@ -4,16 +4,22 @@ require "rails"
|
|
|
4
4
|
|
|
5
5
|
module OpenapiBlocks
|
|
6
6
|
class Railtie < Rails::Railtie # rubocop:disable Style/Documentation
|
|
7
|
-
initializer "openapi_blocks.middleware" do |app|
|
|
8
|
-
app.middleware.use OpenapiBlocks::Middleware
|
|
9
|
-
end
|
|
10
|
-
|
|
11
7
|
initializer "openapi_blocks.autoload", before: :set_autoload_paths do |app|
|
|
12
8
|
app.config.eager_load_paths << app.root.join("app/openapi")
|
|
9
|
+
app.config.eager_load_paths << app.root.join("app/serializers")
|
|
13
10
|
end
|
|
14
11
|
|
|
15
12
|
config.to_prepare do
|
|
16
13
|
Dir[Rails.root.join("app/openapi/**/*.rb")].each { |f| require f }
|
|
14
|
+
Dir[Rails.root.join("app/serializers/**/*.rb")].each { |f| require f }
|
|
15
|
+
|
|
16
|
+
if OpenapiBlocks.configuration.auto_serialize
|
|
17
|
+
[ActionController::Base, ActionController::API].each do |klass|
|
|
18
|
+
klass.include(OpenapiBlocks::AutoSerialize) unless klass.ancestors.include?(OpenapiBlocks::AutoSerialize)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
OpenapiBlocks::Registry.build!
|
|
22
|
+
end
|
|
17
23
|
end
|
|
18
24
|
end
|
|
19
25
|
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenapiBlocks
|
|
4
|
+
module Registry # rubocop:disable Style/Documentation
|
|
5
|
+
@map = {}
|
|
6
|
+
@mutex = Mutex.new
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
def register(model, serializer)
|
|
10
|
+
@mutex.synchronize { @map[model] = serializer }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def resolve(object)
|
|
14
|
+
model = extract_model(object)
|
|
15
|
+
@mutex.synchronize { @map[model] }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def build!
|
|
19
|
+
@mutex.synchronize { @map = {} }
|
|
20
|
+
|
|
21
|
+
serializer_classes.each do |klass|
|
|
22
|
+
register_by_convention(klass)
|
|
23
|
+
register_by_explicit(klass)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def reset!
|
|
28
|
+
@mutex.synchronize { @map = {} }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def serializer_classes
|
|
34
|
+
ObjectSpace.each_object(Class).select { |klass| safe_serializer?(klass) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def register_by_convention(klass)
|
|
38
|
+
model_name = klass_name(klass)
|
|
39
|
+
.gsub(/Serializer$/, "")
|
|
40
|
+
&.split("::")
|
|
41
|
+
&.last
|
|
42
|
+
return if model_name.blank?
|
|
43
|
+
|
|
44
|
+
model = Object.const_get(model_name)
|
|
45
|
+
return unless model < ActiveRecord::Base
|
|
46
|
+
|
|
47
|
+
register(model, klass)
|
|
48
|
+
rescue NameError
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def register_by_explicit(klass)
|
|
53
|
+
return unless klass.respond_to?(:_serializes) && klass._serializes&.any?
|
|
54
|
+
|
|
55
|
+
klass._serializes.each { |model| register(model, klass) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def safe_serializer?(klass)
|
|
59
|
+
klass_name(klass)&.end_with?("Serializer") && klass < OpenapiBlocks::Serializer
|
|
60
|
+
rescue StandardError
|
|
61
|
+
false
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def klass_name(klass)
|
|
65
|
+
Module.instance_method(:name).bind_call(klass)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def extract_model(object)
|
|
69
|
+
case object
|
|
70
|
+
when Array then object.first&.class
|
|
71
|
+
else object.respond_to?(:klass) ? object.klass : object.class
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -180,7 +180,22 @@ module OpenapiBlocks
|
|
|
180
180
|
openapi_name = "#{operation.schema_name}Openapi"
|
|
181
181
|
Object.const_get(openapi_name)
|
|
182
182
|
rescue NameError
|
|
183
|
-
|
|
183
|
+
openapi_classes.find do |klass|
|
|
184
|
+
klass.respond_to?(:_controller_class) && klass._controller_class.present? &&
|
|
185
|
+
controller_name_for(klass._controller_class) == operation.controller
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def openapi_classes
|
|
190
|
+
ObjectSpace.each_object(Class).select do |klass|
|
|
191
|
+
name = Module.instance_method(:name).bind_call(klass)
|
|
192
|
+
name&.end_with?("Openapi") &&
|
|
193
|
+
(klass < OpenapiBlocks::Base || klass < OpenapiBlocks::Controller)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def controller_name_for(controller_class)
|
|
198
|
+
controller_class.to_s.delete_suffix("Controller").underscore
|
|
184
199
|
end
|
|
185
200
|
end
|
|
186
201
|
end
|
|
@@ -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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|