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 @payload_member ||= member(payload) 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