Class: SoberSwag::Compiler::Type

Inherits:
Object
  • Object
show all
Defined in:
lib/sober_swag/compiler/type.rb

Overview

A compiler for swagger-able types.

This class turns Swagger-able types into a schema. This Schema may be:

As such, it compiles all types to all applicable schemas.

While this class compiles one type at a time, it keeps track of the other types needed to describe this schema. It stores these types in a set, available at #found_types.

For example, with a schema like:

class Bar < SoberSwag::InputObject
  attribute :baz, primitive(:String)
end

class Foo < SoberSwag::InputObject
  attribute :bar, Bar
end

If you compile Foo with this class, #found_types will include Bar.

Defined Under Namespace

Classes: TooComplicatedError, TooComplicatedForPathError, TooComplicatedForQueryError

Constant Summary collapse

METADATA_KEYS =

A list of acceptable keys to use as metadata for an object schema. All other metadata keys defined on a type with InputObject.meta will be ignored.

Returns:

  • (Array<Symbol>)

    valid keys.

%i[description deprecated].freeze
DEFAULT_QUERY_SCHEMA_ATTRS =
{ in: :query, style: :deepObject, explode: true }.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(type) ⇒ Type

Create a new compiler for a swagger-able type.

Parameters:

  • type (Class)

    the type to compile



53
54
55
# File 'lib/sober_swag/compiler/type.rb', line 53

def initialize(type)
  @type = type
end

Instance Attribute Details

#typeClass (readonly)

Returns the type we are compiling.

Returns:

  • (Class)

    the type we are compiling.



59
60
61
# File 'lib/sober_swag/compiler/type.rb', line 59

def type
  @type
end

Instance Method Details

#ensure_uncomplicated(key, value) ⇒ Object (private)



301
302
303
304
305
306
307
308
309
# File 'lib/sober_swag/compiler/type.rb', line 301

def ensure_uncomplicated(key, value)
  return if value[:type]

  return value[:oneOf].each { |member| ensure_uncomplicated(key, member) } if value[:oneOf]

  raise TooComplicatedError, <<~ERROR
    Property #{key} has object-schema #{value}, but this type of param should be simple (IE a primitive of some kind)
  ERROR
end

#eql?(other) ⇒ Boolean

Standard ruby equality.

Returns:

  • (Boolean)


144
145
146
# File 'lib/sober_swag/compiler/type.rb', line 144

def eql?(other)
  other.class == self.class && other.type == type
end

#flatten_one_ofs(object) ⇒ Object (private)



224
225
226
227
228
229
230
231
# File 'lib/sober_swag/compiler/type.rb', line 224

def flatten_one_ofs(object)
  case object
  when Nodes::OneOf
    Nodes::OneOf.new(object.deconstruct.uniq)
  else
    object
  end
end

#flatten_oneofs_hash(object) ⇒ Object (private)



281
282
283
284
285
# File 'lib/sober_swag/compiler/type.rb', line 281

def flatten_oneofs_hash(object)
  object.map { |h|
    h[:oneOf] || h
  }.flatten
end

#found_typesSet<Class>

Get a set of all other types needed to compile this type. This set will not include the type being compiled.

Returns:

  • (Set<Class>)


128
129
130
131
132
133
134
# File 'lib/sober_swag/compiler/type.rb', line 128

def found_types
  @found_types ||=
    begin
      (_, found_types) = parsed_result
      found_types
    end
end

#generate_schema_stubObject (private)



181
182
183
184
185
186
187
# File 'lib/sober_swag/compiler/type.rb', line 181

def generate_schema_stub
  if type.is_a?(Class)
    SoberSwag::Compiler::Primitive.new(type).type_hash
  else
    object_schema
  end
end

#hashObject

Standard ruby hashing method. Compilers hash to the same value if they are compiling the same type.



151
152
153
# File 'lib/sober_swag/compiler/type.rb', line 151

def hash
  [self.class, type].hash
end

#make_object_schema(metadata_keys: METADATA_KEYS) ⇒ Object (private)



198
199
200
# File 'lib/sober_swag/compiler/type.rb', line 198

def make_object_schema(metadata_keys: METADATA_KEYS)
  normalize(mapped_type).cata { |e| to_object_schema(e, ) }.merge(object_schema_meta)
end

#mapped_typeObject (private)



177
178
179
# File 'lib/sober_swag/compiler/type.rb', line 177

def mapped_type
  @mapped_type ||= parsed_type.map { |v| SoberSwag::Compiler::Primitive.new(v).type_hash }
end

#normalize(object) ⇒ Object (private)



202
203
204
# File 'lib/sober_swag/compiler/type.rb', line 202

def normalize(object)
  object.cata { |e| rewrite_sums(e) }.cata { |e| flatten_one_ofs(e) }
end

#object_schemaHash

Get back the schema object for the type described.

Returns:

  • (Hash)


74
75
76
77
# File 'lib/sober_swag/compiler/type.rb', line 74

def object_schema
  @object_schema ||=
    make_object_schema
end

#object_schema_metaHash (private)

Get metadata attributes to be used if compiling an object schema.

Returns:

  • (Hash)


161
162
163
164
165
166
167
# File 'lib/sober_swag/compiler/type.rb', line 161

def object_schema_meta
  return {} unless standalone? && type <= SoberSwag::Type::Named

  {
    description: type.description
  }.reject { |_, v| v.nil? }
end

#one_of_to_schema(object) ⇒ Object (private)



268
269
270
271
272
273
274
275
276
277
278
279
# File 'lib/sober_swag/compiler/type.rb', line 268

def one_of_to_schema(object)
  if object.deconstruct.include?({ type: :null })
    rejected = object.deconstruct.reject { |e| e[:type] == :null }
    if rejected.length == 1
      rejected.first.merge(nullable: true)
    else
      { oneOf: flatten_oneofs_hash(rejected), nullable: true }
    end
  else
    { oneOf: flatten_oneofs_hash(object.deconstruct) }
  end
end

#parsed_resultObject

This type, parsed into an AST.



138
139
140
# File 'lib/sober_swag/compiler/type.rb', line 138

def parsed_result
  @parsed_result ||= Parser.new(type_for_parser).run_parser
end

#parsed_typeObject (private)



169
170
171
172
173
174
175
# File 'lib/sober_swag/compiler/type.rb', line 169

def parsed_type
  @parsed_type ||=
    begin
      (parsed,) = parsed_result
      parsed
    end
end

#path_schemaHash

The schema for this type when it is path of the path.

Returns:

Raises:



93
94
95
96
97
98
99
100
# File 'lib/sober_swag/compiler/type.rb', line 93

def path_schema
  path_schema_stub.map do |e|
    ensure_uncomplicated(e[:name], e[:schema])
    e.merge(in: :path)
  end
rescue TooComplicatedError => e
  raise TooComplicatedForPathError, e.message
end

#path_schema_stubObject (private)



287
288
289
290
291
292
293
294
295
296
297
298
299
# File 'lib/sober_swag/compiler/type.rb', line 287

def path_schema_stub
  @path_schema_stub ||=
    make_object_schema(metadata_keys: METADATA_KEYS | %i[style explode])[:properties].map do |k, v|
      # ensure_uncomplicated(k, v)
      {
        name: k,
        schema: v.reject { |key, _| %i[required nullable explode style].include?(key) },
        required: object_schema[:required].include?(k) || false,
        style: v[:style],
        explode: v[:explode]
      }.reject { |_, v2| v2.nil? }
    end
end

#query_schemaHash

The schema for this type when it is part of the query.

Returns:

Raises:



108
109
110
111
112
# File 'lib/sober_swag/compiler/type.rb', line 108

def query_schema
  path_schema_stub.map { |e| DEFAULT_QUERY_SCHEMA_ATTRS.merge(e) }
rescue TooComplicatedError => e
  raise TooComplicatedForQueryError, e.message
end

#ref_nameString

Get the name of this type if it is to be used in a $ref key. This is useful if we are going to use this type compiler to compile an attribute of another object.

Returns:

  • (String)

    a reference specifier for this type



119
120
121
# File 'lib/sober_swag/compiler/type.rb', line 119

def ref_name
  SoberSwag::Compiler::Primitive.new(type).ref_name
end

#rewrite_sums(object) ⇒ Object (private)

rubocop:disable Metrics/MethodLength



206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'lib/sober_swag/compiler/type.rb', line 206

def rewrite_sums(object) # rubocop:disable Metrics/MethodLength
  case object
  when Nodes::Sum
    lhs, rhs = object.deconstruct
    if lhs.is_a?(Nodes::OneOf) && rhs.is_a?(Nodes::OneOf)
      Nodes::OneOf.new(lhs.deconstruct + rhs.deconstruct)
    elsif lhs.is_a?(Nodes::OneOf)
      Nodes::OneOf.new([*lhs.deconstruct, rhs])
    elsif rhs.is_a?(Nodes::OneOf)
      Nodes::OneOf.new([lhs, *rhs.deconstruct])
    else
      Nodes::OneOf.new([lhs, rhs])
    end
  else
    object
  end
end

#schema_stubHash

Give a "stub type" for this schema. This is suitable to use as the schema for attributes of other schemas. Almost always generates a ref object.

Returns:

  • (Hash)

    the OpenAPI V3 schema stub



84
85
86
# File 'lib/sober_swag/compiler/type.rb', line 84

def schema_stub
  @schema_stub ||= generate_schema_stub
end

#standalone?true, false

Is this type standalone, IE, worth serializing on its own in the schemas section of our schema?

Returns:

  • (true, false)


65
66
67
# File 'lib/sober_swag/compiler/type.rb', line 65

def standalone?
  type.is_a?(Class)
end

#to_object_schema(object, metadata_keys = METADATA_KEYS) ⇒ Object (private)

rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity



233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
# File 'lib/sober_swag/compiler/type.rb', line 233

def to_object_schema(object,  = METADATA_KEYS) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
  case object
  when Nodes::List
    { type: :array, items: object.element }
  when Nodes::Enum
    { type: :string, enum: object.values }
  when Nodes::OneOf
    one_of_to_schema(object)
  when Nodes::Object
    # openAPI requires that you give a list of required attributes
    # (which IMO is the *totally* wrong thing to do but whatever)
    # so we must do this garbage
    required = object.deconstruct.filter { |(_, b)| b[:required] }.map(&:first)
    {
      type: :object,
      properties: object.deconstruct.map { |(a, b)|
        [a, b.reject { |k, _| k == :required }]
      }.to_h,
      required: required
    }
  when Nodes::Attribute
    name, req, value, meta = object.deconstruct
    value = value.merge(meta&.select { |k, _| .include?(k) } || {})
    if req
      [name, value.merge(required: true)]
    else
      [name, value]
    end
  when Nodes::Primitive
    object.value.merge(object..select { |k, _| .include?(k) })
  else
    raise ArgumentError, "Got confusing node #{object} (#{object.class})"
  end
end

#type_for_parserObject (private)



189
190
191
192
193
194
195
196
# File 'lib/sober_swag/compiler/type.rb', line 189

def type_for_parser
  if type.is_a?(Class)
    type.schema.type
  else
    # Probably a constrained array
    type
  end
end