pd/rack-schema

View on GitHub
lib/rack/schema.rb

Summary

Maintainability
A
0 mins
Test Coverage
require "rack/schema/version"
require "json-schema"
require "link_header"
require "multi_json"

module Rack
  class Schema
    ValidationError = Class.new(StandardError)

    def initialize(app, options = {}, &handler)
      @app = app

      @handler   = handler
      @handler ||= proc { |errors, env, (status, headers, body)|
        json = ''
        body.each { |s| json.concat s }
        raise ValidationError.new({ errors: errors, body: json }) if errors.any?
      }

      @options = {
        validate_schemas: true,
        swallow_links:    false
      }.merge(options)
    end

    def call(env)
      status, headers, body = @app.call(env)

      link_header  = LinkHeader.parse(headers['Link'])
      schema_links = link_header.links.select do |link|
        link.attrs['rel'] == 'describedby'
      end

      errors = validate(body, schema_links)
      swallow(headers, link_header) if swallow_links?
      response = [status, headers, body]
      @handler.call(errors, env, response) || response
    end

    private

    def validate(body, schema_links)
      schema_links.each_with_object [] do |link, acc|
        json = at_anchor(body, link.attrs['anchor'])

        errs = JSON::Validator.fully_validate link.href, json, {
          validate_schemas: @options[:validate_schemas],
          list: link.attrs.key?('collection')
        }

        acc.push [link.to_s, errs] if errs.any?
      end
    end

    def swallow_links?
      @options[:swallow_links] == true
    end

    def swallow(headers, link_header)
      link_header.links.reject! { |link| link.attrs['rel'] == 'describedby' }
      if link_header.links.any?
        headers['Link'] = link_header.to_s
      else
        headers.delete 'Link'
      end
    end

    def at_anchor(body, anchor)
      flat = ''
      body.each { |s| flat.concat s }

      return flat if anchor.nil? || anchor == '#' || anchor == '#/'

      fragments = anchor.sub(/\A#\//, '').split('/')
      fragments.reduce MultiJson.decode(flat) do |value, fragment|
        case value
        when Hash  then value.fetch(fragment, nil)
        when Array then value.fetch(fragment.to_i, nil)
        end
      end
    end
  end
end