module Praxis # Response spec DSL container class ResponseDefinition attr_reader :name def initialize(response_name, **spec, &block) unless response_name raise Exceptions::InvalidConfiguration.new( "Response name is required for a response specification" ) end @spec = { headers:{} } @name = response_name self.instance_exec(**spec, &block) if block_given? if self.status.nil? raise Exceptions::InvalidConfiguration.new( "Status code is required for a response specification" ) end end def description(text=nil) return @spec[:description] if text.nil? @spec[:description] = text end def status(code=nil) return @spec[:status] if code.nil? @spec[:status] = code end def media_type(media_type=nil) return @spec[:media_type] if media_type.nil? @spec[:media_type] = case media_type when String SimpleMediaType.new(media_type) when Class if media_type < Praxis::Types::MediaTypeCommon media_type else raise Exceptions::InvalidConfiguration.new( 'Invalid media_type specification. media_type must be a Praxis::MediaType' ) end when SimpleMediaType media_type else raise Exceptions::InvalidConfiguration.new( 'Invalid media_type specification. media_type must be a String, MediaType or SimpleMediaType' ) end end def location(loc=nil) return @spec[:location] if loc.nil? unless ( loc.is_a?(Regexp) || loc.is_a?(String) ) raise Exceptions::InvalidConfiguration.new( "Invalid location specification. Location in response must be either a regular expression or a string." ) end @spec[:location] = loc end def headers(hdrs = nil) return @spec[:headers] if hdrs.nil? case hdrs when Array hdrs.each {|header_name| header(header_name) } when Hash header(hdrs) when String header(hdrs) else raise Exceptions::InvalidConfiguration.new( "Invalid headers specification: Arrays, Hash, or String must be used. Received: #{hdrs.inspect}" ) end end def header(hdr) case hdr when String @spec[:headers][hdr] = true when Hash hdr.each do | k, v | unless v.is_a?(Regexp) || v.is_a?(String) raise Exceptions::InvalidConfiguration.new( "Header definitions for #{k.inspect} can only match values of type String or Regexp. Received: #{v.inspect}" ) end @spec[:headers][k] = v end else raise Exceptions::InvalidConfiguration.new( "A header definition can only take a String (to match the name) or" + " a Hash (to match both the name and the value). Received: #{hdr.inspect}" ) end end def describe location_type = location.is_a?(Regexp) ? :regexp : :string location_value = location.is_a?(Regexp) ? location.inspect : location content = { :description => description, :status => status, :headers => {} } content[:location] = _describe_header(location) unless location == nil # TODO: Change the mime_type key to media_type!! if media_type content[:media_type] = if media_type.is_a? Symbol media_type else media_type.describe(true) # TODO: is a shallow describe what we want? or just the name? end end unless headers == nil headers.each do |name, value| content[:headers][name] = _describe_header(value) end end unless parts == nil content[:parts_like] = parts.describe end content end def _describe_header(data) data_type = data.is_a?(Regexp) ? :regexp : :string data_value = data.is_a?(Regexp) ? data.inspect : data { :value => data_value, :type => data_type } end def validate( response ) validate_status!(response) validate_location!(response) validate_headers!(response) validate_content_type!(response) validate_parts!(response) end def parts(proc=nil, like: nil, **args, &block) a_proc = proc || block if like.nil? && !a_proc raise ArgumentError, "Parts definition for response #{name} needs a :like argument or a block/proc" if !args.empty? return @parts end if like && a_proc raise ArgumentError, "Parts definition for response #{name} does not allow :like and a block simultaneously" end if like template = ApiDefinition.instance.response(like) @parts = template.compile(nil, **args) else # block @parts = Praxis::ResponseDefinition.new('anonymous', **args, &a_proc) end end # Validates Status code # # @raise [Exceptions::Validation] When response returns an unexpected status. # def validate_status!(response) return unless status # Validate status code if defined in the spec if response.status != status raise Exceptions::Validation.new( "Invalid response code detected. Response %s dictates status of %s but this response is returning %s." % [name, status.inspect, response.status.inspect] ) end end # Validates 'Location' header # # @raise [Exceptions::Validation] When location header does not match to the defined one. # def validate_location!(response) return if location.nil? || location === response.headers['Location'] raise Exceptions::Validation.new("LOCATION does not match #{location.inspect}") end # Validates Headers # # @raise [Exceptions::Validation] When there is a missing required header.. # def validate_headers!(response) return unless headers headers.each do |name, value| if name.is_a? Symbol raise Exceptions::Validation.new( "Symbols are not supported in headers" ) end unless response.headers.has_key?(name) raise Exceptions::Validation.new( "Header #{name.inspect} was required but is missing" ) end case value when String if response.headers[name] != value unless valid raise Exceptions::Validation.new( "Header #{name.inspect}, with value #{value.inspect} does not match #{response.headers[name]}." ) end end when Regexp if response.headers[name] !~ value unless valid raise Exceptions::Validation.new( "Header #{name.inspect}, with value #{value.inspect} does not match #{response.headers[name].inspect}." ) end end end end end # Validates Content-Type header and response media type # # @param [Object] action # # @raise [Exceptions::Validation] When there is a missing required header # def validate_content_type!(response) return unless media_type response_content_type = {} if response.headers['Content-Type'] response_content_type = Praxis::ContentTypeParser.parse(response.headers['Content-Type']) end expected_content_type = Praxis::ContentTypeParser.parse(media_type.identifier) unless response_content_type[:type] == expected_content_type[:type] raise Exceptions::Validation.new( "Bad Content-Type header. #{response.headers['Content-Type']}" + " does not match type #{expected_content_type[:type]} as described in response: #{self.name}" ) end if (expected_params = expected_content_type[:params]) expected_params.each do |param_name,expected_param| response_param = response_content_type[:params].fetch(param_name) do raise Exceptions::Validation.new( "Bad Content-Type header: #{response.headers['Content-Type']}" + " does not contain expected param '#{param_name}' as described in response: #{self.name}" ) end unless response_param == expected_param raise Exceptions::Validation.new( "Bad Content-Type header: #{response.headers['Content-Type']}" + " param: #{param_name} does not match expected value #{expected_param} as described in response: #{self.name}" ) end end end end def validate_parts!(response) return unless parts response.parts.each do |name, part| parts.validate(part) end end end end