require 'set'
module Seahorse
module Model
module Shapes
@types = {}
class << self
# Registers a shape by type.
#
# Shapes.register('structure', Shapes::StructureShape)
#
# Shapes.type('structure')
# #=> #<Shapes::StructureShape>
#
# @param [String] type
# @param [Class<Shape>] shape_class
# @return [void]
# @raise [ArgumentError] Raises an error if the given type or
# shape class have already been registered.
def register(type, shape_class)
shape_class.type = type
@types[type] = shape_class
end
# Given a type, this method returned the registered shape class.
# @param [String] type
# @return [Class<Shape>]
# @raise [ArgumentError] Raises an ArgumentError if there is no
# shape class registered with the given `type`.
def shape_class(type)
if @types.key?(type)
@types[type]
else
raise ArgumentError, "unregisterd type `#{type}'"
end
end
# Returns an enumerator that yields registered type names and shape
# classes.
#
# Seahorse::Model::Shapes.types.each do |type, shape_class|
# puts "%s => %s" % [type, shape_class.name]
# end
#
# @return [Enumerator] Returns an enumerable object that yields
# registered type names and shape classes.
def types
Enumerator.new do |y|
@types.each do |name, shape_class|
y.yield(name, shape_class)
end
end
end
end
class Shape
# @param [Hash] definition
# @option options [ShapeMap] :shape_map (nil)
def initialize(definition, options = {})
definition['type'] ||= self.class.type
@name = definition['shape']
@definition = definition
@type = definition['type']
@location = definition['location'] || 'body'
@location_name = definition['locationName']
@shape_map = options[:shape_map] || ShapeMap.new
end
# @return [String]
attr_reader :name
# @return [Hash]
attr_reader :definition
# @return [String] The type name for this shape.
attr_reader :type
# @return [String] Returns one of 'body', 'uri', 'headers', 'status_code'
attr_reader :location
# @return [String, nil] Typically only set for shapes that are
# structure members. Serialized names are typically set on the
# shape references, not on the shape definition.
attr_reader :location_name
attr_reader :documentation
# @return [ShapeMap]
attr_reader :shape_map
# @return [String, nil]
def documentation
@definition['documentation']
end
# @param [String] key
# @return [Object, nil]
def metadata(key)
@definition[key.to_s]
end
# @api private
# @return [String]
def inspect
"#<#{self.class.name}>"
end
# @api private
def with(options)
self.class.new(@definition.merge(options), shape_map: shape_map)
end
private
def underscore(string)
Util.underscore(string)
end
def shape_at(key)
if @definition[key]
shape_for(@definition[key])
else
raise ArgumentError, "expected shape definition at #{key.inspect}"
end
end
def shape_for(reference)
if reference.key?('shape')
# shape ref given, e.g. { "shape" => "ShapeName" },
# use the shape map to resolve this reference
@shape_map.shape(reference)
else
Shape.new(reference, shape_map: @shape_map)
end
end
class << self
# @return [String]
attr_accessor :type
# Constructs and returns a new shape object. You must specify
# the shape type using the "type" option or you must construct
# the shape using the appropriate subclass of `Shape`.
#
# @example Constructing a new shape
#
# shape = Seahorse::Model::Shapes::Shape.new("type" => "structure")
#
# shape.class
# #=> Seahorse::Model::Shapes::Structure
#
# shape.definition
# #=> { "type" => "structure" }
#
# @example Constructing a new shape using the shape class
#
# shape = Seahorse::Model::Shapes::String.new
# shape.definition
# #=> { "type" => "string" }
#
# @param [Hash] definition
# @option options [ShapeMap] :shape_map
# @return [Shape]
def new(definition = {}, options = {})
if self == Shape
from_type(definition, options)
else
super(apply_type(definition), options)
end
end
private
def apply_type(definition)
case definition['type']
when type then definition
when nil then { 'type' => type }.merge(definition)
else raise ArgumentError, "expected 'type' to be `#{type}'"
end
end
def from_type(definition, options)
if type = definition['type']
Shapes.shape_class(type).new(definition, options)
else
raise ArgumentError, 'must specify "type" in the definition'
end
end
end
end
class Structure < Shape
def initialize(definition, options = {})
super
@members = {}
@member_refs = {}
@member_names = {}
compute_member_names
compute_required_member_names
@member_names = @member_names.values
end
# @return [Array<Symbol>] Returns a list of members names.
attr_reader :member_names
# @return [Array<Symbol>] Returns a list of required members names.
attr_reader :required
# @return [String, nil] Returns the name of the payload member if set.
attr_reader :payload
# @return [Shape, nil]
def payload_member
if payload
@payload_member ||= member(payload)
else
nil
end
end
# @param [Symbol] name
# @return [Shape]
def member(name)
if ref = @member_refs[name.to_sym]
@members[name] ||= shape_for(ref)
else
raise ArgumentError, "no such member :#{name}"
end
end
# @param [Symbol] name
# @return [Boolean] Returns `true` if this structure has a member with
# the given name.
def member?(name)
@member_refs.key?(name.to_sym)
end
# @return [Enumerable<Symbol,Shape>] Returns an enumerator that yields
# member names and shapes.
def members
Enumerator.new do |y|
member_names.map do |member_name|
y.yield(member_name, member(member_name))
end
end
end
# Searches the structure members for a shape with the given
# serialized name.
#
# If found, the shape will be returned with its symbolized member
# name.
#
# If no shape is found with the given serialized name, then
# nil is returned.
#
# @example
#
# name, shape = structure.member_by_location_name('SerializedName')
# name #=> :member_name
# shape #=> instance of Seahorse::Model::Shapes::Shape
#
# @param [String] location_name
# @return [Array<Symbol,Shape>, nil]
def member_by_location_name(location_name)
@by_location_name ||= index_members_by_location_name
@by_location_name[location_name]
end
private
def index_members_by_location_name
members.each.with_object({}) do |(name, shape), hash|
hash[shape.location_name] = [name, shape]
end
end
def compute_member_names
(definition['members'] || {}).each do |orig_name,ref|
name = underscore(orig_name).to_sym
if ref['location'] == 'headers'
@member_refs[name] = ref
else
@member_refs[name] = { 'locationName' => orig_name }.merge(ref)
end
@member_names[orig_name] = name
end
@payload = @member_names[definition['payload']] if definition['payload']
end
def compute_required_member_names
@required = (definition['required'] || []).map do |orig_name|
@member_names[orig_name]
end
end
end
class List < Shape
def initialize(definition, options = {})
super
@min = definition['min']
@max = definition['max']
@member = shape_at('member')
end
# @return [Shape]
attr_reader :member
# @return [Integer, nil]
attr_reader :min
# @return [Integer, nil]
attr_reader :max
end
class Map < Shape
def initialize(definition, options = {})
super
@min = definition['min']
@max = definition['max']
@key = shape_at('key')
@value = shape_at('value')
end
# @return [Shape]
attr_reader :key
# @return [Shape]
attr_reader :value
# @return [Integer, nil]
attr_reader :min
# @return [Integer, nil]
attr_reader :max
end
class String < Shape
def initialize(definition, options = {})
super
@enum = Set.new(definition['enum']) if definition['enum']
@pattern = definition['pattern']
@min = definition['min']
@max = definition['max']
end
# @return [Set, nil]
attr_reader :enum
# @return [String, nil]
attr_reader :pattern
# @return [Integer, nil]
attr_reader :min
# @return [Integer, nil]
attr_reader :max
end
class Character < String; end
class Byte < String; end
class Timestamp < Shape
def initialize(definition, options = {})
@format = definition['timestampFormat']
super
end
# @return [String]
attr_reader :format
# @param [Time] time
# @param [String] default_format The format to default to
# when {#format} is not set on this timestamp shape.
# @return [String]
def format_time(time, default_format)
format = @format || default_format
case format
when 'iso8601' then time.utc.iso8601
when 'rfc822' then time.utc.rfc822
when 'httpdate' then time.httpdate
when 'unixTimestamp' then time.utc.to_i
else
msg = "invalid timestamp format #{format.inspect}"
raise ArgumentError, msg
end
end
end
class Integer < Shape
def initialize(definition, options = {})
@min = definition['min']
@max = definition['max']
super
end
# @return [Integer, nil]
attr_reader :min
# @return [Integer, nil]
attr_reader :max
end
class Long < Integer; end
class Float < Shape; end
class Double < Float; end
class Boolean < Shape; end
class Blob < Shape; end
register('blob', Shapes::Blob)
register('byte', Shapes::Byte)
register('boolean', Shapes::Boolean)
register('character', Shapes::Character)
register('double', Shapes::Double)
register('float', Shapes::Float)
register('integer', Shapes::Integer)
register('list', Shapes::List)
register('long', Shapes::Long)
register('map', Shapes::Map)
register('string', Shapes::String)
register('structure', Shapes::Structure)
register('timestamp', Shapes::Timestamp)
end
end
end