class Praxis::Types::MultipartArray

Attributes

attributes[R]
identifier[R]
multiple[R]
options[R]
content_type[R]
preamble[RW]

Public Class Methods

as_json_schema(attribute_options: {}, **_other) click to toggle source
# File lib/praxis/types/multipart_array.rb, line 206
def self.as_json_schema(attribute_options: {}, **_other)
  as_openapi_request_body(attribute_options: attribute_options)
end
as_openapi_request_body(attribute_options: {}) click to toggle source

Multipart request bodies are special in OPEN API schema: # Request payload type: object properties: # Request parts

id:            # Part 1 (string value)
  type: string
  format: uuid
address:       # Part2 (object)
  type: object
  properties:
    street:
      type: string
    city:
      type: string
profileImage:  # Part 3 (an image)
  type: string
  format: binary

NOTE: not sure if this

# File lib/praxis/types/multipart_array.rb, line 171
def self.as_openapi_request_body(attribute_options: {})
  hash = { type: json_schema_type }
  opts = options.merge(attribute_options)
  hash[:description] = opts[:description] if opts[:description]
  hash[:default] = opts[:default] if opts[:default]

  unless attributes.empty?
    props = {}
    encoding = {}
    attributes.each do |part_name, part_attribute|
      part_example = part_attribute.example
      key_to_use = part_name.is_a?(Regexp) ? part_name.source : part_name

      if (payload_attribute = part_attribute.options[:payload_attribute])
        props[key_to_use] = payload_attribute.as_json_schema(example: part_example.payload)
      end
      # {
      # contentType: 'fff',
      # headers: {
      #   custom1: 'safd'
      # }
      next unless (headers_attribute = part_attribute.options[:headers_attribute])

      # Does this 'Content-Type' string check work?...can it be a symbol? what does it mean anyway?
      encoding[key_to_use][:contentType] = headers_attribute['Content-Type'] if headers_attribute['Content-Type']
      # TODO?rethink? ...is this correct?: att a 'headers' key with some header schemas if this part have some
      encoding[key_to_use]['headers'] = headers_attribute.as_json_schema(example: part_example.headers)
    end

    hash[:properties] = props if props.presence
    hash[:encoding] = encoding unless encoding.empty?
  end
  hash
end
construct(constructor_block, _options = {}) click to toggle source
# File lib/praxis/types/multipart_array.rb, line 43
def self.construct(constructor_block, _options = {})
  Class.new(self, &constructor_block)
end
constructable?() click to toggle source
# File lib/praxis/types/multipart_array.rb, line 39
def self.constructable?
  true
end
describe(shallow = true, example: nil) click to toggle source
# File lib/praxis/types/multipart_array.rb, line 210
def self.describe(shallow = true, example: nil)
  type_name = Attributor.type_name(self)
  hash = {
    name: type_name.gsub(Attributor::MODULE_PREFIX_REGEX, ''),
    family: family,
    id: id
  }
  hash[:example] = example if example

  hash[:part_name] = { type: name_type.describe(true) }

  unless shallow
    hash[:attributes] = {} if attributes.keys.any? { |name| name.is_a? ::String }
    hash[:pattern_attributes] = {} if attributes.keys.any? { |name| name.is_a? Regexp }

    if hash.key?(:attributes) || hash.key?(:pattern_attributes)
      describe_attributes(shallow, example: example).each do |name, sub_hash|
        case name
        when ::String
          hash[:attributes][name] = sub_hash
        when Regexp
          hash[:pattern_attributes][name.source] = sub_hash
        end
      end
    else
      hash[:part_payload] = { type: payload_type.describe(true) }
    end
  end
  hash
end
describe_attributes(shallow = true, example: nil) click to toggle source
# File lib/praxis/types/multipart_array.rb, line 241
def self.describe_attributes(shallow = true, example: nil)
  attributes.each_with_object({}) do |(part_name, part_attribute), parts|
    sub_example = example.part(part_name) if example
    sub_example = sub_example.first if sub_example && multiple.include?(part_name)

    sub_hash = part_attribute.describe(shallow, example: sub_example)

    if (options = sub_hash.delete(:options))
      sub_hash[:options] = {}
      sub_hash[:options][:multiple] = true if multiple.include?(part_name)

      if (payload_attribute = options.delete :payload_attribute) && payload_attribute.options[:required]
        sub_hash[:options][:required] = true
      end
    end

    sub_hash[:type] = MultipartPart.describe(shallow, example: sub_example, options: part_attribute.options)

    parts[part_name] = sub_hash
  end
end
dump(value, **opts) click to toggle source
# File lib/praxis/types/multipart_array.rb, line 359
def self.dump(value, **opts)
  value.dump(**opts)
end
dump_for_openapi(example) click to toggle source
# File lib/praxis/types/multipart_array.rb, line 374
def self.dump_for_openapi(example)
  example.map { |part| MultipartPart.dump_for_openapi(part) }
end
example(context = Attributor::DEFAULT_ROOT_CONTEXT, **_options) click to toggle source
# File lib/praxis/types/multipart_array.rb, line 126
def self.example(context = Attributor::DEFAULT_ROOT_CONTEXT, **_options)
  example = new

  attributes.each do |name, attribute|
    next if name.is_a? Regexp

    sub_context = generate_subcontext(context, name)

    part = attribute.example(sub_context)
    part.name = name
    example.push part

    next unless multiple.include? name

    part = attribute.example(sub_context + ['2'])
    part.name = name
    example.push part
  end

  example
end
file(name, payload_type = nil, **opts, &block) click to toggle source
# File lib/praxis/types/multipart_array.rb, line 105
def self.file(name, payload_type = nil, **opts, &block)
  part(name, payload_type, filename: true, **opts, &block)
end
inherited(klass) click to toggle source
Calls superclass method
# File lib/praxis/types/multipart_array.rb, line 16
def self.inherited(klass)
  super
  klass.instance_eval do
    @attributes = FuzzyHash.new
    @saved_blocks = []
    @multiple = []
    @options = {}

    @payload_type = Attributor::String
    @name_type = Attributor::String
    @member_type = Praxis::MultipartPart

    @payload_attribute = nil
    @part_attribute = nil

    @identifier = MediaTypeIdentifier.load('multipart/form-data').freeze
  end
end
json_schema_type() click to toggle source
# File lib/praxis/types/multipart_array.rb, line 148
def self.json_schema_type
  :object
end
load(value, _context = Attributor::DEFAULT_ROOT_CONTEXT, content_type: nil) click to toggle source
# File lib/praxis/types/multipart_array.rb, line 109
def self.load(value, _context = Attributor::DEFAULT_ROOT_CONTEXT, content_type: nil)
  return value if value.is_a?(self) || value.nil?

  raise ArgumentError, "content_type is required to load values of type String for #{Attributor.type_name(self)}" if value.is_a?(::String) && content_type.nil?

  parser = Praxis::MultipartParser.new({ 'Content-Type' => content_type }, value)
  preamble, parts = parser.parse

  instance = new
  instance.push(*parts)

  instance.preamble = preamble
  instance.content_type = content_type

  instance
end
name_type(type = nil) click to toggle source
# File lib/praxis/types/multipart_array.rb, line 47
def self.name_type(type = nil)
  return @name_type if type.nil?

  @name_type = Attributor.resolve_type type
end
new(content_type: self.class.identifier.to_s) click to toggle source
Calls superclass method
# File lib/praxis/types/multipart_array.rb, line 266
def initialize(content_type: self.class.identifier.to_s)
  super()
  self.content_type = content_type
end
part(name, payload_type = nil, multiple: false, filename: false, **opts, &block) click to toggle source
# File lib/praxis/types/multipart_array.rb, line 73
def self.part(name, payload_type = nil, multiple: false, filename: false, **opts, &block)
  @attributes.default_proc = nil

  if name.is_a?(Regexp)
    raise 'part with regexp name may not take :multiple option' if multiple
    raise 'part with regexp name may not be required' if opts[:required] == true
  end

  self.multiple << name if multiple

  compiler = Attributor::DSLCompiler.new(self, **opts)

  filename_attribute = compiler.define('filename', String, required: true) if filename

  if block_given?
    definition = PartDefinition.new(&block)
    payload_attribute = definition.payload_attribute
    header_attribute = definition.headers_attribute
    filename_attribute = definition.filename_attribute || filename_attribute

    attributes[name] = compiler.define(name, Praxis::MultipartPart,
                                       payload_attribute: payload_attribute,
                                       headers_attribute: header_attribute,
                                       filename_attribute: filename_attribute)
  else
    payload_attribute = compiler.define(name, payload_type || self.payload_type, **opts)
    attributes[name] = compiler.define(name, Praxis::MultipartPart,
                                       payload_attribute: payload_attribute,
                                       filename_attribute: filename_attribute)
  end
end
part_attribute() click to toggle source
# File lib/praxis/types/multipart_array.rb, line 69
def self.part_attribute
  @part_attribute ||= Attributor::Attribute.new(Praxis::MultipartPart, payload_attribute: payload_attribute)
end
payload_attribute() click to toggle source
# File lib/praxis/types/multipart_array.rb, line 65
def self.payload_attribute
  @payload_attribute ||= Attributor::Attribute.new(@payload_type)
end
payload_type(type = nil, **opts, &block) click to toggle source
# File lib/praxis/types/multipart_array.rb, line 53
def self.payload_type(type = nil, **opts, &block)
  if type.nil?
    return @payload_type unless block_given?

    type = Attributor::Struct
  end
  @payload_type = Attributor.resolve_type(type)
  @payload_attribute = Attributor::Attribute.new(@payload_type, **opts, &block)
  @part_attribute = nil
  @payload_type
end

Public Instance Methods

content_type=(content_type) click to toggle source
# File lib/praxis/types/multipart_array.rb, line 271
def content_type=(content_type)
  @content_type = MediaTypeIdentifier.load(content_type)
  @content_type.parameters.set 'boundary', 'Boundary_puppies' if @content_type.parameters.get('boundary').nil?
  @content_type
end
dump(**opts) click to toggle source
# File lib/praxis/types/multipart_array.rb, line 363
def dump(**opts)
  boundary = content_type.parameters.get 'boundary'

  parts = collect do |part|
    part.dump(**opts)
  end

  all_entities = parts.join("\r\n--#{boundary}\r\n")
  "--#{boundary}\r\n#{all_entities}\r\n--#{boundary}--\r\n"
end
part(name) click to toggle source
# File lib/praxis/types/multipart_array.rb, line 319
def part(name)
  if self.class.multiple.include?(name)
    self.select { |i| i.name == name }
  else
    find { |i| i.name == name }
  end
end
part?(name) click to toggle source
# File lib/praxis/types/multipart_array.rb, line 327
def part?(name)
  any? { |i| i.name == name }
end
payload_type() click to toggle source
# File lib/praxis/types/multipart_array.rb, line 277
def payload_type
  self.class.payload_type
end
push(*parts, context: Attributor::DEFAULT_ROOT_CONTEXT) click to toggle source
# File lib/praxis/types/multipart_array.rb, line 281
def push(*parts, context: Attributor::DEFAULT_ROOT_CONTEXT)
  part, *rest = parts
  return push(part, context: context).push(*rest, context: context) if rest.any?

  original_context = context

  part.name = self.class.name_type.load(part.name, self.class.generate_subcontext(context, part.name))
  key = part.name

  context = self.class.generate_subcontext(context, key)

  # If no attributes are defined, we always use the default
  # payload_attribute, otherwise we constrain the parts
  # to the defined names.
  attribute = if self.class.attributes.empty?
                self.class.part_attribute
              elsif (default_thingy = self.class.attributes[key])
                default_thingy
              end

  if attribute
    part.attribute = attribute
    part.load_payload(context + ['payload'])
    part.load_headers(context + ['headers'])
    return self << part
  elsif self.class.options[:case_insensitive_load]
    name = self.class.attributes.keys.find do |k|
      k.is_a?(::String) && key.downcase == k.downcase
    end
    if name
      part.name = name
      return push(part, context: original_context)
    end
  end

  raise Attributor::AttributorException, "Unknown part name received: #{key.inspect} while loading #{Attributor.humanize_context(context)}"
end
validate(context = Attributor::DEFAULT_ROOT_CONTEXT) click to toggle source
# File lib/praxis/types/multipart_array.rb, line 331
def validate(context = Attributor::DEFAULT_ROOT_CONTEXT)
  errors = each_with_index.each_with_object([]) do |(part, idx), error_list|
    sub_context = if part.name
                    self.class.generate_subcontext(context, part.name)
                  else
                    context + ["at(#{idx})"]
                  end

    error_list.push(*part.validate(sub_context))
  end

  self.class.attributes.each do |name, attribute|
    payload_attribute = attribute.options[:payload_attribute]

    if !part?(name)
      if payload_attribute.options[:required]
        sub_context = self.class.generate_subcontext(context, name)
        errors.push "Attribute #{Attributor.humanize_context(sub_context)} is required"
      end
      # Return, don't bother checking nullability as it hasn't been provided
    elsif !self.part(name) && !Attribute.nullable_attribute?(payload_attribute.options)
      sub_context = self.class.generate_subcontext(context, name)
      errors.push "Attribute #{Attributor.humanize_context(sub_context)} is not nullable"
    end
  end
  errors
end