Skip to content

Commit 6621f72

Browse files
committed
Implement dynamic parsing
1 parent 9e34215 commit 6621f72

7 files changed

Lines changed: 255 additions & 145 deletions

File tree

generator/src/main/java/line/bot/generator/LineBotSdkRubyGenerator.java

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
import java.io.File;
44
import java.util.HashMap;
5+
import java.util.List;
56
import java.util.Map;
67
import java.util.Optional;
8+
import java.util.stream.Collectors;
79

810
import org.openapitools.codegen.CodegenDiscriminator;
911
import org.openapitools.codegen.CodegenModel;
@@ -79,19 +81,42 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs)
7981
for (ModelsMap entry : result.values()) {
8082
for (ModelMap mo : entry.getModels()) {
8183
CodegenModel cm = mo.getModel();
84+
final Map<String, Object> selector = new HashMap<>();
8285

8386
if (cm.getParentModel() != null) {
8487
final CodegenDiscriminator discriminator = cm.getParentModel().getDiscriminator();
8588
final Optional<String> mappingNameOptional = discriminator.getMappedModels().stream().filter(
8689
it -> it.getModelName().equals(cm.name)
8790
).map(CodegenDiscriminator.MappedModel::getMappingName).findFirst();
8891
mappingNameOptional.ifPresent(mappingName -> {
89-
final Map<String, Object> selector = new HashMap<>();
9092
selector.put("propertyName", discriminator.getPropertyName());
9193
selector.put("mappingName", mappingName);
9294
cm.getVendorExtensions().put("x-selector", selector);
9395
});
9496
}
97+
98+
if (cm.getChildren() != null && cm.getDiscriminator() != null) {
99+
final List<Map<String, String>> childMappings =
100+
cm.getChildren().stream()
101+
.map(child -> {
102+
Map<String, String> childInfo = new HashMap<>();
103+
childInfo.put("className", child.name);
104+
105+
final CodegenDiscriminator discriminator = cm.getDiscriminator();
106+
if (discriminator != null) {
107+
discriminator.getMappedModels().stream()
108+
.filter(mappedModel -> mappedModel.getModelName().equals(child.name))
109+
.findFirst()
110+
.ifPresent(mappedModel -> childInfo.put("typeName", mappedModel.getMappingName()));
111+
}
112+
113+
return childInfo;
114+
})
115+
.collect(Collectors.toList());
116+
117+
cm.getVendorExtensions().put("x-children", childMappings);
118+
cm.getVendorExtensions().put("x-discriminator-property", cm.getDiscriminator().getPropertyName());
119+
}
95120
}
96121
}
97122
return result;

generator/src/main/java/line/bot/generator/LineBotSdkRubyRbsGenerator.java

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
import java.io.File;
44
import java.util.HashMap;
5+
import java.util.List;
56
import java.util.Map;
67
import java.util.Optional;
8+
import java.util.stream.Collectors;
79

810
import org.openapitools.codegen.CodegenDiscriminator;
911
import org.openapitools.codegen.CodegenModel;
@@ -80,19 +82,42 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs)
8082
for (ModelsMap entry : result.values()) {
8183
for (ModelMap mo : entry.getModels()) {
8284
CodegenModel cm = mo.getModel();
85+
final Map<String, Object> selector = new HashMap<>();
8386

8487
if (cm.getParentModel() != null) {
8588
final CodegenDiscriminator discriminator = cm.getParentModel().getDiscriminator();
8689
final Optional<String> mappingNameOptional = discriminator.getMappedModels().stream().filter(
8790
it -> it.getModelName().equals(cm.name)
8891
).map(CodegenDiscriminator.MappedModel::getMappingName).findFirst();
8992
mappingNameOptional.ifPresent(mappingName -> {
90-
final Map<String, Object> selector = new HashMap<>();
9193
selector.put("propertyName", discriminator.getPropertyName());
9294
selector.put("mappingName", mappingName);
9395
cm.getVendorExtensions().put("x-selector", selector);
9496
});
9597
}
98+
99+
if (cm.getChildren() != null && cm.getDiscriminator() != null) {
100+
final List<Map<String, String>> childMappings =
101+
cm.getChildren().stream()
102+
.map(child -> {
103+
Map<String, String> childInfo = new HashMap<>();
104+
childInfo.put("className", child.name);
105+
106+
final CodegenDiscriminator discriminator = cm.getDiscriminator();
107+
if (discriminator != null) {
108+
discriminator.getMappedModels().stream()
109+
.filter(mappedModel -> mappedModel.getModelName().equals(child.name))
110+
.findFirst()
111+
.ifPresent(mappedModel -> childInfo.put("typeName", mappedModel.getMappingName()));
112+
}
113+
114+
return childInfo;
115+
})
116+
.collect(Collectors.toList());
117+
118+
cm.getVendorExtensions().put("x-children", childMappings);
119+
cm.getVendorExtensions().put("x-discriminator-property", cm.getDiscriminator().getPropertyName());
120+
}
96121
}
97122
}
98123
return result;

generator/src/main/resources/line-bot-sdk-ruby-generator/model.pebble

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,57 @@ module Line
2525
{% if model.model.vendorExtensions.get("x-selector").propertyName != property.name %}attr_accessor{% else %}attr_reader{% endif %} :{{ property.name }}{% if property.description != null %} # {{ property.description }}{% endif %}
2626
{%- endfor %}
2727

28-
def initialize({% for property in model.model.vars %}{% if model.model.vendorExtensions.get("x-selector").propertyName != property.name or packageName == 'webhook' %}
28+
def initialize({% for property in model.model.vars %}{% if model.model.vendorExtensions.get("x-selector").propertyName != property.name %}
2929
{{ property.name }}:{% if property.defaultValue == null %}{{ property.required ? '' : ' nil' }}{% else %}{{ ' ' + property.defaultValue }}{% endif %},{% endif %}{% endfor %}
3030
**dynamic_attributes
3131
)
32-
{% if model.model.vendorExtensions.get("x-selector") != null %}@{{model.model.vendorExtensions.get("x-selector").propertyName}} = "{{model.model.vendorExtensions.get("x-selector").mappingName}}"{% endif -%}
32+
{% if model.model.vendorExtensions.get("x-selector") != null %}@{{model.model.vendorExtensions.get("x-selector").propertyName}} = "{{model.model.vendorExtensions.get("x-selector").mappingName}}"{%- endif -%}
3333
{%- for property in model.model.vars %}
34-
{% if model.model.vendorExtensions.get("x-selector").propertyName != property.name %}@{{ property.name }} = {{ property.name }}{% endif -%}
35-
{% endfor %}
34+
{% if property.isArray -%}
35+
@{{ property.name }} = {{ property.name }}{{ property.required ? '' : '&' }}.map do |item|
36+
if item.is_a?(Hash)
37+
Line::Bot::V2::{{ packageName | camelize }}::{{ property.complexType }}.create(**item)
38+
else
39+
item
40+
end
41+
end
42+
{%- elseif property.isModel -%}
43+
@{{ property.name }} = {{ property.name }}.is_a?(Line::Bot::V2::{{ packageName | camelize }}::{{ property.baseType }}){% if not property.required %} || {{ property.name }}.nil?{% endif %} ? {{ property.name }} : Line::Bot::V2::{{ packageName | camelize }}::{{ property.baseType }}.create(**{{ property.name }})
44+
{%- elseif model.model.vendorExtensions.get("x-selector").propertyName != property.name -%}
45+
@{{ property.name }} = {{ property.name }}
46+
{%- endif -%}
47+
{%- endfor %}
3648

3749
dynamic_attributes.each do |key, value|
3850
self.class.attr_accessor key
39-
instance_variable_set("@#{key}", value)
51+
52+
if value.is_a?(Hash)
53+
struct_klass = Struct.new(*value.keys.map(&:to_sym))
54+
struct_values = value.map { |_k, v| v.is_a?(Hash) ? Line::Bot::V2::Utils.hash_to_struct(v) : v }
55+
instance_variable_set("@#{key}", struct_klass.new(*struct_values))
56+
else
57+
instance_variable_set("@#{key}", value)
58+
end
4059
end
4160
end
61+
62+
def self.create(args){% if model.model.vendorExtensions.get("x-children") != null and model.model.vendorExtensions.get("x-discriminator-property") != null %}
63+
klass = detect_class(args[:{{ model.model.vendorExtensions.get("x-discriminator-property") }}])
64+
return klass.new(**args) if klass
65+
{% endif %}
66+
return new(**args)
67+
end
68+
{%- if model.model.vendorExtensions.get("x-children") != null %}
69+
70+
private
71+
72+
def self.detect_class(type)
73+
{
74+
{%- for child in model.model.vendorExtensions.get("x-children") %}
75+
{{ child.typeName }}: Line::Bot::V2::{{ packageName | camelize }}::{{ child.className }},
76+
{%- endfor %}
77+
}[type.to_sym]
78+
end{% endif %}
4279
end
4380
end
4481
end

generator/src/main/resources/line-bot-sdk-ruby-rbs-generator/model.pebble

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,14 @@ module Line
5858
{% endif %}{{ loop.last ? '' : ",
5959
" }}{% endif %}{% endfor %}
6060
) -> void
61+
62+
def self.create: (args: Hash[Symbol, untyped]) -> {{ model.model.classname }}
63+
{%- if model.model.vendorExtensions.get("x-children") != null %}
64+
65+
private
66+
67+
def self.detect_class: (type: String) -> Class
68+
{%- endif %}
6169
{%- endif %}
6270
end
6371
end

lib/line/bot/v2/utils.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,20 @@ def self.deep_underscore(hash)
1717
end
1818
end
1919

20+
def self.deep_symbolize(object)
21+
case object
22+
when Hash
23+
object.each_with_object({}) do |(key, value), new_hash|
24+
sym_key = key.is_a?(String) ? key.to_sym : key
25+
new_hash[sym_key] = deep_symbolize(value)
26+
end
27+
when Array
28+
object.map { |element| deep_symbolize(element) }
29+
else
30+
object
31+
end
32+
end
33+
2034
def self.deep_to_hash(object)
2135
if object.is_a?(Array)
2236
object.map { |item| deep_to_hash(item) }
@@ -62,6 +76,30 @@ def self.deep_compact(object)
6276
end
6377
end
6478

79+
def self.deep_convert_reserved_words(object)
80+
case object
81+
when Hash
82+
object.each_with_object({}) do |(key, value), new_hash|
83+
new_key = if Line::Bot::V2::RESERVED_WORDS.include?(key.to_sym)
84+
"_#{key}"
85+
else
86+
key
87+
end
88+
new_hash[new_key] = deep_convert_reserved_words(value)
89+
end
90+
when Array
91+
object.map { |element| deep_convert_reserved_words(element) }
92+
else
93+
object
94+
end
95+
end
96+
97+
def self.hash_to_struct(hash)
98+
struct_klass = Struct.new(*hash.keys.map(&:to_sym))
99+
struct_values = hash.map { |_k, v| v.is_a?(Hash) ? hash_to_struct(v) : v }
100+
struct_klass.new(*struct_values)
101+
end
102+
65103
def self.camelize(str)
66104
str.to_s.split('_').inject { |memo, word| memo + word.capitalize }
67105
end

lib/line/bot/v2/webhook_parser.rb

Lines changed: 3 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -58,19 +58,11 @@ def parse(body, signature)
5858

5959
data = JSON.parse(body.chomp, symbolize_names: true)
6060
data = Line::Bot::V2::Utils.deep_underscore(data)
61+
data = Line::Bot::V2::Utils.deep_convert_reserved_words(data)
62+
data = Line::Bot::V2::Utils.deep_symbolize(data)
6163

6264
data[:events].map do |event|
63-
event_class_name = determine_class_name(:events, event)
64-
event_class = begin
65-
Object.const_get(event_class_name)
66-
rescue StandardError
67-
nil
68-
end
69-
70-
# If there is no specific webhook class, leave the value as is
71-
event_instance = event_class ? create_instance(event_class, event) : event
72-
73-
deep_hash_to_struct(event_instance)
65+
Line::Bot::V2::Webhook::Event.create(**event)
7466
end
7567
end
7668

@@ -96,133 +88,6 @@ def secure_compare(a, b)
9688
b.each_byte { |byte| res |= byte ^ l.shift }
9789
res == 0
9890
end
99-
100-
def create_instance(klass, attributes)
101-
keyword_args = {}
102-
103-
attributes.each do |key, value|
104-
if value.is_a?(Hash)
105-
nested_class_name = determine_class_name(key, value)
106-
if nested_class_name
107-
nested_klass = Object.const_get(nested_class_name)
108-
value = create_instance(nested_klass, value)
109-
end
110-
elsif value.is_a?(Array)
111-
value = value.map do |item|
112-
nested_class_name = determine_class_name(key, item)
113-
if nested_class_name
114-
nested_klass = Object.const_get(nested_class_name)
115-
create_instance(nested_klass, item)
116-
else
117-
item
118-
end
119-
end
120-
end
121-
122-
# CodeGen add _ prefix to reserved words
123-
# see: https://github.com/OpenAPITools/openapi-generator/blob/master/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractRubyCodegen.java
124-
if Line::Bot::V2::RESERVED_WORDS.include?(key)
125-
keyword_args["_#{key}".to_sym] = value
126-
else
127-
keyword_args[key] = value
128-
end
129-
end
130-
131-
begin
132-
klass.new(**keyword_args)
133-
rescue ArgumentError => e
134-
attributes # Return the original hash if unknown keyword error occurs
135-
end
136-
end
137-
138-
# TODO: Derive classes from rbs information or define attribute/class mappings for each class and derive classes from them.
139-
def determine_class_name(key, value)
140-
class_name = if key == :events && value.is_a?(Hash) && value[:type] == 'delivery'
141-
'PnpDeliveryCompletionEvent'
142-
elsif key == :message && value.is_a?(Hash) && value[:type]
143-
pascalize(value[:type]) + 'MessageContent'
144-
elsif key == :mentionees && value.is_a?(Hash) && value[:type]
145-
pascalize(value[:type]) + 'Mentionee'
146-
elsif key == :module && value.is_a?(Hash) && value[:type]
147-
pascalize(value[:type]) + 'ModuleContent'
148-
elsif key == :things && value.is_a?(Hash) && value[:type]
149-
pascalize(value[:type]) + 'ThingsContent'
150-
elsif value.is_a?(Hash) && value[:type] && ['user', 'group', 'room'].include?(value[:type])
151-
pascalize(value[:type]) + 'Source'
152-
elsif key == :result && value.is_a?(Hash) && value[:result_code]
153-
'ScenarioResult'
154-
elsif key == :content_provider # This has type, but there is no typed ContentProvider
155-
'ContentProvider'
156-
elsif key == :_module # This has type, but there is no typed ModuleContent
157-
'ModuleContent'
158-
elsif key == :unsend
159-
'UnsendDetail'
160-
elsif key == :follow
161-
'FollowDetail'
162-
elsif key == :joined
163-
'JoinedMembers'
164-
elsif key == :left
165-
'LeftMembers'
166-
elsif key == :postback
167-
'PostbackContent'
168-
elsif key == :beacon
169-
'BeaconContent'
170-
elsif key == :link
171-
'LinkContent'
172-
elsif key == :delivery
173-
'PnpDelivery'
174-
elsif value.is_a?(Hash) && value[:type] # Event etc.
175-
pascalize(value[:type]) + singularize(pascalize(key))
176-
elsif value.is_a?(Array) && key.to_s.end_with?('s')
177-
pascalize(singularize(key))
178-
else
179-
singularize(pascalize(key))
180-
end
181-
182-
full_class_name = "Line::Bot::V2::Webhook::#{class_name}"
183-
Object.const_defined?(full_class_name) ? full_class_name : nil
184-
end
185-
186-
def pascalize(str)
187-
str.to_s.gsub(/(?:^|_)([a-z])/) { ::Regexp.last_match(1).upcase }
188-
end
189-
190-
def singularize(str)
191-
str = str.to_s
192-
193-
if str.end_with?('ies')
194-
str.sub(/ies$/, 'y')
195-
elsif str.end_with?('ves')
196-
str.sub(/ves$/, 'f')
197-
elsif str.end_with?('oes') || str.end_with?('xes') || str.end_with?('ses')
198-
str.sub(/es$/, '')
199-
elsif str.end_with?('s')
200-
str.sub(/s$/, '')
201-
else
202-
str
203-
end
204-
end
205-
206-
def deep_hash_to_struct(obj)
207-
case obj
208-
when Hash
209-
keys = obj.keys.map(&:to_sym)
210-
struct_class = Struct.new(*keys)
211-
struct_class.new(*obj.values.map { |value| deep_hash_to_struct(value) })
212-
when Array
213-
obj.map { |item| deep_hash_to_struct(item) }
214-
else
215-
if obj.respond_to?(:instance_variables)
216-
obj.instance_variables.each do |var|
217-
value = obj.instance_variable_get(var)
218-
if value.is_a?(Hash) || value.is_a?(Array) || value.respond_to?(:instance_variables)
219-
obj.instance_variable_set(var, deep_hash_to_struct(value))
220-
end
221-
end
222-
end
223-
obj
224-
end
225-
end
22691
end
22792
end
22893
end

0 commit comments

Comments
 (0)