|
1 | 1 | {{#description}} |
2 | 2 | # {{{.}}} |
3 | 3 | {{/description}} |
4 | | - module {{classname}} |
| 4 | + class {{classname}} |
| 5 | + include JSON::Serializable |
| 6 | + include YAML::Serializable |
| 7 | + |
| 8 | + class SchemaMismatchError < Exception |
| 9 | + end |
| 10 | + |
5 | 11 | {{#oneOf}} |
6 | 12 | {{#-first}} |
7 | 13 | # List of class defined in oneOf (OpenAPI v3) |
8 | 14 | def self.openapi_one_of |
9 | 15 | [ |
10 | 16 | {{/-first}} |
11 | | - :"{{{.}}}"{{^-last}},{{/-last}} |
| 17 | + {{{.}}}{{^-last}},{{/-last}} |
12 | 18 | {{#-last}} |
13 | 19 | ] |
14 | 20 | end |
15 | 21 |
|
16 | 22 | {{/-last}} |
17 | 23 | {{/oneOf}} |
18 | | - {{#discriminator}} |
19 | | - {{#propertyName}} |
20 | | - # Discriminator's property name (OpenAPI v3) |
21 | | - def self.openapi_discriminator_name |
22 | | - :"{{{.}}}" |
23 | | - end |
24 | 24 |
|
25 | | - {{/propertyName}} |
26 | | - {{#mappedModels}} |
27 | | - {{#-first}} |
28 | | - # Discriminator's mapping (OpenAPI v3) |
29 | | - def self.openapi_discriminator_mapping |
30 | | - { |
31 | | - {{/-first}} |
32 | | - :"{{{mappingName}}}" => :"{{{modelName}}}"{{^-last}},{{/-last}} |
33 | | - {{#-last}} |
34 | | - } |
35 | | - end |
36 | | - |
37 | | - {{/-last}} |
38 | | - {{/mappedModels}} |
39 | | - {{/discriminator}} |
40 | | - # Builds the object |
41 | | - # @param [Mixed] Data to be matched against the list of oneOf items |
42 | | - # @return [Object] Returns the model or the data itself |
43 | | - def self.build(data) |
44 | 25 | {{#discriminator}} |
45 | | - discriminator_value = data[openapi_discriminator_name] |
46 | | - return nil unless discriminator_value |
47 | | - {{#mappedModels}} |
48 | | - {{#-first}} |
49 | | - |
50 | | - klass = openapi_discriminator_mapping[discriminator_value.to_sym] |
51 | | - return nil unless klass |
52 | | - |
53 | | - {{moduleName}}.const_get(klass).build_from_hash(data) |
54 | | - {{/-first}} |
55 | | - {{/mappedModels}} |
56 | | - {{^mappedModels}} |
57 | | - {{moduleName}}.const_get(discriminator_value).build_from_hash(data) |
58 | | - {{/mappedModels}} |
| 26 | + use_yaml_discriminator {{#propertyName}}"{{{.}}}"{{/propertyName}}, { |
| 27 | + {{#mappedModels}}{{{mappingName}}}: {{{modelName}}}{{^-last}},{{/-last}}{{/mappedModels}} |
| 28 | + } |
59 | 29 | {{/discriminator}} |
60 | | - {{^discriminator}} |
61 | | - # Go through the list of oneOf items and attempt to identify the appropriate one. |
62 | | - # Note: |
63 | | - # - We do not attempt to check whether exactly one item matches. |
64 | | - # - No advanced validation of types in some cases (e.g. "x: { type: string }" will happily match { x: 123 }) |
65 | | - # due to the way the deserialization is made in the base_object template (it just casts without verifying). |
66 | | - # - TODO: scalar values are de facto behaving as if they were nullable. |
67 | | - # - TODO: logging when debugging is set. |
| 30 | + |
| 31 | + def self.build(data) |
68 | 32 | openapi_one_of.each do |klass| |
69 | 33 | begin |
70 | | - next if klass == :AnyType # "nullable: true" |
71 | 34 | typed_data = find_and_cast_into_type(klass, data) |
72 | 35 | return typed_data if typed_data |
73 | | - rescue # rescue all errors so we keep iterating even if the current item lookup raises |
| 36 | + rescue ex |
| 37 | + # rescue all errors so we keep iterating even if the current item lookup raises |
| 38 | + Log.trace { ex.message } |
74 | 39 | end |
75 | 40 | end |
76 | 41 |
|
77 | | - openapi_one_of.includes?(:AnyType) ? data : nil |
78 | | - {{/discriminator}} |
| 42 | + nil |
79 | 43 | end |
80 | | - {{^discriminator}} |
81 | 44 |
|
82 | | - SchemaMismatchError = Class.new(StandardError) |
83 | | - |
84 | | - # Note: 'File' is missing here because in the regular case we get the data _after_ a call to JSON.parse. |
85 | | - private def self.find_and_cast_into_type(klass, data) |
| 45 | + {{#oneOf}} |
| 46 | + private def self.find_and_cast_into_type(klass : {{{.}}}.class, data) |
86 | 47 | return if data.nil? |
87 | 48 |
|
88 | | - begin |
89 | | - case klass.to_s |
90 | | - when "Boolean" |
91 | | - return data if data.instance_of?(TrueClass) || data.instance_of?(FalseClass) |
92 | | - when "Float" |
93 | | - return data if data.instance_of?(Float) |
94 | | - when "Integer" |
95 | | - return data if data.instance_of?(Integer) |
96 | | - when "Time" |
97 | | - return Time.parse(data) |
98 | | - when "Date" |
99 | | - return Date.parse(data) |
100 | | - when "String" |
101 | | - return data if data.instance_of?(String) |
102 | | - when "Object" # "type: object" |
103 | | - return data if data.instance_of?(Hash) |
104 | | - when /\AArray<(?<sub_type>.+)>\z/ # "type: array" |
105 | | - if data.instance_of?(Array) |
106 | | - sub_type = Regexp.last_match[:sub_type] |
107 | | - return data.map { |item| find_and_cast_into_type(sub_type, item) } |
108 | | - end |
109 | | - when /\AHash<String, (?<sub_type>.+)>\z/ # "type: object" with "additionalProperties: { ... }" |
110 | | - if data.instance_of?(Hash) && data.keys.all? { |k| k.instance_of?(Symbol) || k.instance_of?(String) } |
111 | | - sub_type = Regexp.last_match[:sub_type] |
112 | | - return data.each_with_object({} of String | Symbol => Bool | Float | Integer | Time | Date | String | Array | Hash) { |(k, v), hsh| hsh[k] = find_and_cast_into_type(sub_type, v) } |
113 | | - end |
114 | | - else # model |
115 | | - const = {{moduleName}}.const_get(klass) |
116 | | - if const |
117 | | - if const.respond_to?(:openapi_one_of) # nested oneOf model |
118 | | - model = const.build(data) |
119 | | - return model if model |
120 | | - else |
121 | | - # raise if data contains keys that are not known to the model |
122 | | - raise unless (data.keys - const.acceptable_attributes).empty? |
123 | | - model = const.build_from_hash(data) |
124 | | - return model if model && model.valid? |
125 | | - end |
126 | | - end |
| 49 | + Log.trace { "INSPECTING DATA" } |
| 50 | + Log.trace { data.inspect } |
| 51 | + |
| 52 | + case data |
| 53 | + when NetboxClient::RecursiveHash |
| 54 | + if (value = cast_value(array_data: false, array_class: array_class?(klass), klass: klass, data: data)) |
| 55 | + return new(value) |
| 56 | + end |
| 57 | + when Array(NetboxClient::RecursiveHash) |
| 58 | + if (value = cast_value(array_data: true, array_class: array_class?(klass), klass: klass, data: data)) |
| 59 | + return new(value) |
127 | 60 | end |
| 61 | + else |
| 62 | + raise SchemaMismatchError.new("#{data} doesn't match the #{klass} type") |
| 63 | + end |
| 64 | + end |
| 65 | + {{/oneOf}} |
| 66 | + |
| 67 | + private def self.cast_value(array_data : Bool, array_class : Bool, klass, data) |
| 68 | + if array_class == true && array_data == true |
| 69 | + Log.debug { "Building array of classes: #{klass} / #{data}" } |
128 | 70 |
|
129 | | - raise # if no match by now, raise |
130 | | - rescue |
131 | | - raise SchemaMismatchError, "#{data} doesn't match the #{klass} type" |
| 71 | + klass.from_json(data.to_json) |
| 72 | + elsif array_class == false && array_data == false |
| 73 | + Log.debug { "Building single class: #{klass} / #{data}" } |
| 74 | + |
| 75 | + klass.from_json(data.to_json) |
| 76 | + end |
| 77 | + end |
| 78 | + |
| 79 | + private def self.array_class?(klass) |
| 80 | + klass.name.starts_with?("Array(") |
| 81 | + end |
| 82 | + |
| 83 | + {{#oneOf}} |
| 84 | + def initialize(@value : {{{.}}}) |
| 85 | + end |
| 86 | + |
| 87 | + {{/oneOf}} |
| 88 | + |
| 89 | + delegate :to_yaml, to: @value |
| 90 | + delegate :to_json, to: @value |
| 91 | + |
| 92 | + def to_any_h |
| 93 | + {"value" => to_h} |
| 94 | + end |
| 95 | + |
| 96 | + def to_h |
| 97 | + val = @value |
| 98 | + if val.is_a?(Int32) |
| 99 | + val |
| 100 | + else |
| 101 | + val.to_h |
132 | 102 | end |
133 | 103 | end |
134 | | - {{/discriminator}} |
135 | 104 | end |
0 commit comments