committee 0.4.14 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/bin/committee-stub +1 -1
- data/lib/committee/errors.rb +1 -10
- data/lib/committee/middleware/base.rb +9 -1
- data/lib/committee/middleware/request_validation.rb +5 -9
- data/lib/committee/middleware/response_validation.rb +3 -19
- data/lib/committee/middleware/stub.rb +4 -5
- data/lib/committee/request_validator.rb +28 -0
- data/lib/committee/response_generator.rb +19 -13
- data/lib/committee/response_validator.rb +21 -88
- data/lib/committee/router.rb +10 -9
- data/lib/committee/test/methods.rb +15 -17
- data/lib/committee.rb +10 -3
- data/test/middleware/request_validation_test.rb +23 -43
- data/test/middleware/response_validation_test.rb +9 -27
- data/test/middleware/stub_test.rb +10 -1
- data/test/request_validator_test.rb +34 -0
- data/test/response_generator_test.rb +15 -15
- data/test/response_validator_test.rb +19 -110
- data/test/router_test.rb +6 -6
- data/test/test/methods_test.rb +7 -20
- data/test/test_helper.rb +4 -40
- metadata +14 -17
- data/lib/committee/param_validator.rb +0 -106
- data/lib/committee/schema.rb +0 -56
- data/lib/committee/validation.rb +0 -83
- data/test/param_validator_test.rb +0 -127
- data/test/performance/request_validation.rb +0 -55
- data/test/schema_test.rb +0 -36
    
        data/bin/committee-stub
    CHANGED
    
    | @@ -32,8 +32,8 @@ schema = File.read(args[0]) | |
| 32 32 |  | 
| 33 33 | 
             
            app = Rack::Builder.new {
         | 
| 34 34 | 
             
              use Committee::Middleware::RequestValidation, schema: schema
         | 
| 35 | 
            -
              use Committee::Middleware::Stub, schema: schema
         | 
| 36 35 | 
             
              use Committee::Middleware::ResponseValidation, schema: schema
         | 
| 36 | 
            +
              use Committee::Middleware::Stub, schema: schema
         | 
| 37 37 | 
             
              run lambda { |_|
         | 
| 38 38 | 
             
                [404, {}, ["Not found"]]
         | 
| 39 39 | 
             
              }
         | 
    
        data/lib/committee/errors.rb
    CHANGED
    
    | @@ -5,16 +5,7 @@ module Committee | |
| 5 5 | 
             
              class BadRequest < Error
         | 
| 6 6 | 
             
              end
         | 
| 7 7 |  | 
| 8 | 
            -
              class  | 
| 9 | 
            -
              end
         | 
| 10 | 
            -
             | 
| 11 | 
            -
              class InvalidFormat < Error
         | 
| 12 | 
            -
              end
         | 
| 13 | 
            -
             | 
| 14 | 
            -
              class InvalidType < Error
         | 
| 15 | 
            -
              end
         | 
| 16 | 
            -
             | 
| 17 | 
            -
              class InvalidParams < Error
         | 
| 8 | 
            +
              class InvalidRequest < Error
         | 
| 18 9 | 
             
              end
         | 
| 19 10 |  | 
| 20 11 | 
             
              class InvalidResponse < Error
         | 
| @@ -5,7 +5,11 @@ module Committee::Middleware | |
| 5 5 |  | 
| 6 6 | 
             
                  @params_key = options[:params_key] || "committee.params"
         | 
| 7 7 | 
             
                  data = options[:schema] || raise("need option `schema`")
         | 
| 8 | 
            -
                   | 
| 8 | 
            +
                  if data.is_a?(String)
         | 
| 9 | 
            +
                    warn_string_deprecated
         | 
| 10 | 
            +
                    data = MultiJson.decode(data)
         | 
| 11 | 
            +
                  end
         | 
| 12 | 
            +
                  @schema = JsonSchema.parse!(data)
         | 
| 9 13 | 
             
                  @router = Committee::Router.new(@schema)
         | 
| 10 14 | 
             
                end
         | 
| 11 15 |  | 
| @@ -15,5 +19,9 @@ module Committee::Middleware | |
| 15 19 | 
             
                  [status, { "Content-Type" => "application/json" },
         | 
| 16 20 | 
             
                    [MultiJson.encode({ id: id, error: message }, pretty: true)]]
         | 
| 17 21 | 
             
                end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                def warn_string_deprecated
         | 
| 24 | 
            +
                  Committee.warn_deprecated("Committee: passing a string to `schema` option is deprecated; please send a deserialized hash instead.")
         | 
| 25 | 
            +
                end
         | 
| 18 26 | 
             
              end
         | 
| 19 27 | 
             
            end
         | 
| @@ -2,21 +2,17 @@ module Committee::Middleware | |
| 2 2 | 
             
              class RequestValidation < Base
         | 
| 3 3 | 
             
                def initialize(app, options={})
         | 
| 4 4 | 
             
                  super
         | 
| 5 | 
            -
                  @allow_extra = options[:allow_extra]
         | 
| 6 5 | 
             
                  @prefix = options[:prefix]
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                  # deprecated
         | 
| 8 | 
            +
                  @allow_extra = options[:allow_extra]
         | 
| 7 9 | 
             
                end
         | 
| 8 10 |  | 
| 9 11 | 
             
                def call(env)
         | 
| 10 12 | 
             
                  request = Rack::Request.new(env)
         | 
| 11 13 | 
             
                  env[@params_key] = Committee::RequestUnpacker.new(request).call
         | 
| 12 | 
            -
                  link | 
| 13 | 
            -
             | 
| 14 | 
            -
                    Committee::ParamValidator.new(
         | 
| 15 | 
            -
                      env[@params_key],
         | 
| 16 | 
            -
                      @schema,
         | 
| 17 | 
            -
                      link,
         | 
| 18 | 
            -
                      allow_extra: @allow_extra
         | 
| 19 | 
            -
                    ).call
         | 
| 14 | 
            +
                  if link = @router.routes_request?(request, prefix: @prefix)
         | 
| 15 | 
            +
                    Committee::RequestValidator.new.call(link, env[@params_key])
         | 
| 20 16 | 
             
                  end
         | 
| 21 17 | 
             
                  @app.call(env)
         | 
| 22 18 | 
             
                rescue Committee::BadRequest
         | 
| @@ -8,17 +8,10 @@ module Committee::Middleware | |
| 8 8 | 
             
                def call(env)
         | 
| 9 9 | 
             
                  status, headers, response = @app.call(env)
         | 
| 10 10 | 
             
                  request = Rack::Request.new(env)
         | 
| 11 | 
            -
                   | 
| 12 | 
            -
                    @router.routes_request?(request, prefix: @prefix)
         | 
| 13 | 
            -
                  if type_schema
         | 
| 14 | 
            -
                    check_content_type!(headers)
         | 
| 11 | 
            +
                  if link = @router.routes_request?(request, prefix: @prefix)
         | 
| 15 12 | 
             
                    str = response.reduce("") { |str, s| str << s }
         | 
| 16 | 
            -
                     | 
| 17 | 
            -
             | 
| 18 | 
            -
                      @schema,
         | 
| 19 | 
            -
                      link_schema,
         | 
| 20 | 
            -
                      type_schema
         | 
| 21 | 
            -
                    ).call
         | 
| 13 | 
            +
                    data = MultiJson.decode(str)
         | 
| 14 | 
            +
                    Committee::ResponseValidator.new(link).call(headers, data)
         | 
| 22 15 | 
             
                  end
         | 
| 23 16 | 
             
                  [status, headers, response]
         | 
| 24 17 | 
             
                rescue Committee::InvalidResponse
         | 
| @@ -26,14 +19,5 @@ module Committee::Middleware | |
| 26 19 | 
             
                rescue MultiJson::LoadError
         | 
| 27 20 | 
             
                  render_error(500, :invalid_response, "Response wasn't valid JSON.")
         | 
| 28 21 | 
             
                end
         | 
| 29 | 
            -
             | 
| 30 | 
            -
                private
         | 
| 31 | 
            -
             | 
| 32 | 
            -
                def check_content_type!(headers)
         | 
| 33 | 
            -
                  unless headers["Content-Type"] =~ %r{application/json}
         | 
| 34 | 
            -
                    raise Committee::InvalidResponse,
         | 
| 35 | 
            -
                      %{"Content-Type" response header must be set to "application/json".}
         | 
| 36 | 
            -
                  end
         | 
| 37 | 
            -
                end
         | 
| 38 22 | 
             
              end
         | 
| 39 23 | 
             
            end
         | 
| @@ -9,11 +9,10 @@ module Committee::Middleware | |
| 9 9 |  | 
| 10 10 | 
             
                def call(env)
         | 
| 11 11 | 
             
                  request = Rack::Request.new(env)
         | 
| 12 | 
            -
                   | 
| 13 | 
            -
                  if type_schema
         | 
| 12 | 
            +
                  if link = @router.routes_request?(request, prefix: @prefix)
         | 
| 14 13 | 
             
                    headers = { "Content-Type" => "application/json" }
         | 
| 15 | 
            -
                    data = cache( | 
| 16 | 
            -
                      Committee::ResponseGenerator.new( | 
| 14 | 
            +
                    data = cache(link.method, link.href) do
         | 
| 15 | 
            +
                      Committee::ResponseGenerator.new.call(link)
         | 
| 17 16 | 
             
                    end
         | 
| 18 17 | 
             
                    if @call
         | 
| 19 18 | 
             
                      env["committee.response"] = data
         | 
| @@ -28,7 +27,7 @@ module Committee::Middleware | |
| 28 27 | 
             
                      # made, and stub normally
         | 
| 29 28 | 
             
                      headers.merge!(call_headers)
         | 
| 30 29 | 
             
                    end
         | 
| 31 | 
            -
                    status =  | 
| 30 | 
            +
                    status = link.rel == "create" ? 201 : 200
         | 
| 32 31 | 
             
                    [status, headers, [MultiJson.encode(data, pretty: true)]]
         | 
| 33 32 | 
             
                  else
         | 
| 34 33 | 
             
                    @app.call(env)
         | 
| @@ -0,0 +1,28 @@ | |
| 1 | 
            +
            module Committee
         | 
| 2 | 
            +
              class RequestValidator
         | 
| 3 | 
            +
                def initialize(options = {})
         | 
| 4 | 
            +
                end
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                def call(link, params)
         | 
| 7 | 
            +
                  if link.schema
         | 
| 8 | 
            +
                    valid, errors = link.schema.validate(params)
         | 
| 9 | 
            +
                    if !valid
         | 
| 10 | 
            +
                      errors = error_messages(errors).join("\n")
         | 
| 11 | 
            +
                      raise InvalidRequest, "Invalid request.\n\n#{errors}"
         | 
| 12 | 
            +
                    end
         | 
| 13 | 
            +
                  end
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                private
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                def error_messages(errors)
         | 
| 19 | 
            +
                  errors.map do |error|
         | 
| 20 | 
            +
                    if error.schema
         | 
| 21 | 
            +
                      %{At "#{error.schema.uri}": #{error.message}}
         | 
| 22 | 
            +
                    else
         | 
| 23 | 
            +
                      error.message
         | 
| 24 | 
            +
                    end
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
              end
         | 
| 28 | 
            +
            end
         | 
| @@ -1,29 +1,35 @@ | |
| 1 1 | 
             
            module Committee
         | 
| 2 2 | 
             
              class ResponseGenerator
         | 
| 3 | 
            -
                def  | 
| 4 | 
            -
                   | 
| 5 | 
            -
             | 
| 6 | 
            -
                   | 
| 7 | 
            -
             | 
| 3 | 
            +
                def call(link)
         | 
| 4 | 
            +
                  data = generate_properties(link.parent)
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                  # list is a special case; wrap data in an array
         | 
| 7 | 
            +
                  data = [data] if link.rel == "instances"
         | 
| 8 8 |  | 
| 9 | 
            -
             | 
| 10 | 
            -
                  generate_properties(@type_schema)
         | 
| 9 | 
            +
                  data
         | 
| 11 10 | 
             
                end
         | 
| 12 11 |  | 
| 13 12 | 
             
                private
         | 
| 14 13 |  | 
| 15 14 | 
             
                def generate_properties(schema)
         | 
| 16 15 | 
             
                  data = {}
         | 
| 17 | 
            -
                  schema | 
| 18 | 
            -
                    data[ | 
| 16 | 
            +
                  schema.properties.each do |key, value|
         | 
| 17 | 
            +
                    data[key] = if !value.properties.empty?
         | 
| 19 18 | 
             
                      generate_properties(value)
         | 
| 20 19 | 
             
                    else
         | 
| 21 | 
            -
                       | 
| 22 | 
            -
                       | 
| 20 | 
            +
                      # special example attribute was included; use its value
         | 
| 21 | 
            +
                      if !value.data["example"].nil?
         | 
| 22 | 
            +
                        value.data["example"]
         | 
| 23 | 
            +
                      # null is allowed; use that
         | 
| 24 | 
            +
                      elsif value.type.include?("null")
         | 
| 25 | 
            +
                        nil
         | 
| 26 | 
            +
                      # otherwise we don't know what to do (we could eventually generate
         | 
| 27 | 
            +
                      # random data based on type/format
         | 
| 28 | 
            +
                      else
         | 
| 29 | 
            +
                        raise(%{At "#{schema.id}"/"#{key}": no "example" attribute and "null" is not allowed; don't know how to generate property.})
         | 
| 30 | 
            +
                      end
         | 
| 23 31 | 
             
                    end
         | 
| 24 32 | 
             
                  end
         | 
| 25 | 
            -
                  # list is a special case; wrap data in an array
         | 
| 26 | 
            -
                  data = [data] if @link_schema["title"] == "List"
         | 
| 27 33 | 
             
                  data
         | 
| 28 34 | 
             
                end
         | 
| 29 35 | 
             
              end
         | 
| @@ -1,111 +1,44 @@ | |
| 1 1 | 
             
            module Committee
         | 
| 2 2 | 
             
              class ResponseValidator
         | 
| 3 | 
            -
                 | 
| 4 | 
            -
             | 
| 5 | 
            -
             | 
| 6 | 
            -
             | 
| 7 | 
            -
                  @data = data
         | 
| 8 | 
            -
                  @schema = schema
         | 
| 9 | 
            -
                  @link_schema = link_schema
         | 
| 10 | 
            -
                  @type_schema = type_schema
         | 
| 3 | 
            +
                def initialize(link)
         | 
| 4 | 
            +
                  @link = link
         | 
| 5 | 
            +
                  @validator = JsonSchema::Validator.new(link.parent)
         | 
| 11 6 | 
             
                end
         | 
| 12 7 |  | 
| 13 | 
            -
                def call
         | 
| 14 | 
            -
                   | 
| 15 | 
            -
             | 
| 8 | 
            +
                def call(headers, data)
         | 
| 9 | 
            +
                  check_content_type!(headers)
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  if @link.rel == "instances"
         | 
| 12 | 
            +
                    if !data.is_a?(Array)
         | 
| 16 13 | 
             
                      raise InvalidResponse, "List endpoints must return an array of objects."
         | 
| 17 14 | 
             
                    end
         | 
| 18 15 | 
             
                    # only consider the first object during the validation from here on
         | 
| 19 | 
            -
                     | 
| 20 | 
            -
                  else
         | 
| 21 | 
            -
                    @data
         | 
| 22 | 
            -
                  end
         | 
| 23 | 
            -
             | 
| 24 | 
            -
                  data_keys = build_data_keys(data)
         | 
| 25 | 
            -
                  schema_keys = build_schema_keys
         | 
| 26 | 
            -
             | 
| 27 | 
            -
                  extra = data_keys - schema_keys
         | 
| 28 | 
            -
                  missing = schema_keys - data_keys
         | 
| 29 | 
            -
             | 
| 30 | 
            -
                  errors = []
         | 
| 31 | 
            -
             | 
| 32 | 
            -
                  if extra.count > 0
         | 
| 33 | 
            -
                    errors << "Extra keys in response: #{extra.join(', ')}."
         | 
| 34 | 
            -
                  end
         | 
| 35 | 
            -
             | 
| 36 | 
            -
                  if missing.count > 0
         | 
| 37 | 
            -
                    errors << "Missing keys in response: #{missing.join(', ')}."
         | 
| 16 | 
            +
                    data = data[0]
         | 
| 38 17 | 
             
                  end
         | 
| 39 18 |  | 
| 40 | 
            -
                   | 
| 41 | 
            -
                     | 
| 42 | 
            -
             | 
| 43 | 
            -
             | 
| 44 | 
            -
                  check_data!(@type_schema, data, [])
         | 
| 45 | 
            -
                end
         | 
| 46 | 
            -
             | 
| 47 | 
            -
                def check_data!(schema, data, path)
         | 
| 48 | 
            -
                  schema["properties"].each do |key, value|
         | 
| 49 | 
            -
                    if value["properties"]
         | 
| 50 | 
            -
                      check_data!(value, data[key], path + [key])
         | 
| 51 | 
            -
                    elsif value["type"] == ["array"]
         | 
| 52 | 
            -
                      definition = @schema.find(value["items"]["$ref"])
         | 
| 53 | 
            -
                      data[key].each do |datum|
         | 
| 54 | 
            -
                        check_type!(definition["type"], datum, path + [key])
         | 
| 55 | 
            -
                        check_data!(definition, datum, path + [key]) if definition["type"] == ["object"]
         | 
| 56 | 
            -
                        unless definition["type"].include?("null") && datum.nil?
         | 
| 57 | 
            -
                          check_format!(definition["format"], datum, path + [key])
         | 
| 58 | 
            -
                          check_pattern!(definition["pattern"], datum, path + [key])
         | 
| 59 | 
            -
                        end
         | 
| 60 | 
            -
                      end
         | 
| 61 | 
            -
             | 
| 62 | 
            -
                    else
         | 
| 63 | 
            -
                      definition = @schema.find(value["$ref"])
         | 
| 64 | 
            -
                      check_type!(definition["type"], data[key], path + [key])
         | 
| 65 | 
            -
                      unless definition["type"].include?("null") && data[key].nil?
         | 
| 66 | 
            -
                        check_format!(definition["format"], data[key], path + [key])
         | 
| 67 | 
            -
                        check_pattern!(definition["pattern"], data[key], path + [key])
         | 
| 68 | 
            -
                      end
         | 
| 69 | 
            -
                    end
         | 
| 19 | 
            +
                  if !@validator.validate(data)
         | 
| 20 | 
            +
                    errors = error_messages(@validator.errors).join("\n")
         | 
| 21 | 
            +
                    raise InvalidResponse, "Invalid response.\n\n#{errors}"
         | 
| 70 22 | 
             
                  end
         | 
| 71 23 | 
             
                end
         | 
| 72 24 |  | 
| 73 25 | 
             
                private
         | 
| 74 26 |  | 
| 75 | 
            -
                def  | 
| 76 | 
            -
                   | 
| 77 | 
            -
             | 
| 78 | 
            -
             | 
| 79 | 
            -
                      keys += value.keys.map { |k| "#{key}:#{k}" }
         | 
| 80 | 
            -
                    else
         | 
| 81 | 
            -
                      keys << key
         | 
| 82 | 
            -
                    end
         | 
| 27 | 
            +
                def check_content_type!(headers)
         | 
| 28 | 
            +
                  unless headers["Content-Type"] =~ %r{application/json}
         | 
| 29 | 
            +
                    raise Committee::InvalidResponse,
         | 
| 30 | 
            +
                      %{"Content-Type" response header must be set to "application/json".}
         | 
| 83 31 | 
             
                  end
         | 
| 84 | 
            -
                  keys
         | 
| 85 32 | 
             
                end
         | 
| 86 33 |  | 
| 87 | 
            -
                def  | 
| 88 | 
            -
                   | 
| 89 | 
            -
             | 
| 90 | 
            -
             | 
| 91 | 
            -
                      info
         | 
| 92 | 
            -
                    elsif info["type"] == ["array"]
         | 
| 93 | 
            -
                      array_schema = @schema.find(info["items"]["$ref"])
         | 
| 94 | 
            -
                      unless array_schema["type"] == ["object"]
         | 
| 95 | 
            -
                        array_schema
         | 
| 96 | 
            -
                      else
         | 
| 97 | 
            -
                        {} # satisfy data['properties'] check below
         | 
| 98 | 
            -
                      end
         | 
| 99 | 
            -
                    elsif info["$ref"]
         | 
| 100 | 
            -
                      @schema.find(info["$ref"])
         | 
| 101 | 
            -
                    end
         | 
| 102 | 
            -
                    if data["properties"]
         | 
| 103 | 
            -
                      keys += data["properties"].keys.map { |k| "#{key}:#{k}" }
         | 
| 34 | 
            +
                def error_messages(errors)
         | 
| 35 | 
            +
                  errors.map do |error|
         | 
| 36 | 
            +
                    if error.schema
         | 
| 37 | 
            +
                      %{At "#{error.schema.uri}": #{error.message}}
         | 
| 104 38 | 
             
                    else
         | 
| 105 | 
            -
                       | 
| 39 | 
            +
                      error.message
         | 
| 106 40 | 
             
                    end
         | 
| 107 41 | 
             
                  end
         | 
| 108 | 
            -
                  keys
         | 
| 109 42 | 
             
                end
         | 
| 110 43 | 
             
              end
         | 
| 111 44 | 
             
            end
         | 
    
        data/lib/committee/router.rb
    CHANGED
    
    | @@ -7,13 +7,13 @@ module Committee | |
| 7 7 | 
             
                def routes?(method, path, options = {})
         | 
| 8 8 | 
             
                  path = path.gsub(/^#{options[:prefix]}/, "") if options[:prefix]
         | 
| 9 9 | 
             
                  if method_routes = @routes[method]
         | 
| 10 | 
            -
                    method_routes.each do |pattern, link | 
| 10 | 
            +
                    method_routes.each do |pattern, link|
         | 
| 11 11 | 
             
                      if path =~ pattern
         | 
| 12 | 
            -
                        return link | 
| 12 | 
            +
                        return link
         | 
| 13 13 | 
             
                      end
         | 
| 14 14 | 
             
                    end
         | 
| 15 15 | 
             
                  end
         | 
| 16 | 
            -
                   | 
| 16 | 
            +
                  nil
         | 
| 17 17 | 
             
                end
         | 
| 18 18 |  | 
| 19 19 | 
             
                def routes_request?(request, options = {})
         | 
| @@ -24,13 +24,14 @@ module Committee | |
| 24 24 |  | 
| 25 25 | 
             
                def build_routes(schema)
         | 
| 26 26 | 
             
                  routes = {}
         | 
| 27 | 
            -
                   | 
| 28 | 
            -
             | 
| 29 | 
            -
             | 
| 27 | 
            +
                  # realistically, we should be examining links recursively at all levels
         | 
| 28 | 
            +
                  schema.properties.each do |_, type_schema|
         | 
| 29 | 
            +
                    type_schema.links.each do |link|
         | 
| 30 | 
            +
                      method = link.method.to_s.upcase
         | 
| 31 | 
            +
                      routes[method] ||= []
         | 
| 30 32 | 
             
                      # /apps/{id} --> /apps/([^/]+)
         | 
| 31 | 
            -
                      href = link | 
| 32 | 
            -
                      routes[ | 
| 33 | 
            -
                        [%r{^#{href}$}, link, type_schema]
         | 
| 33 | 
            +
                      href = link.href.gsub(/\{(.*?)\}/, "[^/]+")
         | 
| 34 | 
            +
                      routes[method] << [%r{^#{href}$}, link]
         | 
| 34 35 | 
             
                    end
         | 
| 35 36 | 
             
                  end
         | 
| 36 37 | 
             
                  routes
         | 
| @@ -1,48 +1,46 @@ | |
| 1 1 | 
             
            module Committee::Test
         | 
| 2 2 | 
             
              module Methods
         | 
| 3 3 | 
             
                def assert_schema_conform
         | 
| 4 | 
            -
                   | 
| 4 | 
            +
                  if (data = schema_contents).is_a?(String)
         | 
| 5 | 
            +
                    warn_string_deprecated
         | 
| 6 | 
            +
                    data = MultiJson.decode(data)
         | 
| 7 | 
            +
                  end
         | 
| 5 8 |  | 
| 6 | 
            -
                  @schema ||=  | 
| 9 | 
            +
                  @schema ||= JsonSchema.parse!(data)
         | 
| 7 10 | 
             
                  @router ||= Committee::Router.new(@schema)
         | 
| 8 11 |  | 
| 9 | 
            -
                   | 
| 12 | 
            +
                  link =
         | 
| 10 13 | 
             
                    @router.routes_request?(last_request, prefix: schema_url_prefix)
         | 
| 11 | 
            -
             | 
| 12 | 
            -
                  unless link_schema
         | 
| 14 | 
            +
                  unless link
         | 
| 13 15 | 
             
                    response = "`#{last_request.request_method} #{last_request.path_info}` undefined in schema."
         | 
| 14 16 | 
             
                    raise Committee::InvalidResponse.new(response)
         | 
| 15 17 | 
             
                  end
         | 
| 16 18 |  | 
| 17 19 | 
             
                  data = MultiJson.decode(last_response.body)
         | 
| 18 | 
            -
                  Committee::ResponseValidator.new(
         | 
| 19 | 
            -
                    data,
         | 
| 20 | 
            -
                    @schema,
         | 
| 21 | 
            -
                    link_schema,
         | 
| 22 | 
            -
                    type_schema
         | 
| 23 | 
            -
                  ).call
         | 
| 20 | 
            +
                  Committee::ResponseValidator.new(link).call(last_response.headers, data)
         | 
| 24 21 | 
             
                end
         | 
| 25 22 |  | 
| 26 23 | 
             
                def assert_schema_content_type
         | 
| 27 | 
            -
                   | 
| 28 | 
            -
                    raise Committee::InvalidResponse,
         | 
| 29 | 
            -
                      %{"Content-Type" response header must be set to "application/json".}
         | 
| 30 | 
            -
                  end
         | 
| 24 | 
            +
                  Committee.warn_deprecated("Use of #assert_schema_content_type is deprecated; use #assert_schema_conform instead.")
         | 
| 31 25 | 
             
                end
         | 
| 32 26 |  | 
| 33 27 | 
             
                # can be overridden alternatively to #schema_path in case the schema is
         | 
| 34 28 | 
             
                # easier to access as a string
         | 
| 35 29 | 
             
                # blob
         | 
| 36 30 | 
             
                def schema_contents
         | 
| 37 | 
            -
                  File.read(schema_path)
         | 
| 31 | 
            +
                  MultiJson.decode(File.read(schema_path))
         | 
| 38 32 | 
             
                end
         | 
| 39 33 |  | 
| 40 34 | 
             
                def schema_path
         | 
| 41 | 
            -
                  raise "Please override #schema_path."
         | 
| 35 | 
            +
                  raise "Please override #schema_contents or #schema_path."
         | 
| 42 36 | 
             
                end
         | 
| 43 37 |  | 
| 44 38 | 
             
                def schema_url_prefix
         | 
| 45 39 | 
             
                  nil
         | 
| 46 40 | 
             
                end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                def warn_string_deprecated
         | 
| 43 | 
            +
                  Committee.warn_deprecated("Committee: returning a string from `#schema_contents` is deprecated; please return a deserialized hash instead.")
         | 
| 44 | 
            +
                end
         | 
| 47 45 | 
             
              end
         | 
| 48 46 | 
             
            end
         | 
    
        data/lib/committee.rb
    CHANGED
    
    | @@ -1,14 +1,13 @@ | |
| 1 | 
            +
            require "json_schema"
         | 
| 1 2 | 
             
            require "multi_json"
         | 
| 2 3 | 
             
            require "rack"
         | 
| 3 4 |  | 
| 4 5 | 
             
            require_relative "committee/errors"
         | 
| 5 | 
            -
            require_relative "committee/validation"
         | 
| 6 | 
            -
            require_relative "committee/param_validator"
         | 
| 7 6 | 
             
            require_relative "committee/request_unpacker"
         | 
| 7 | 
            +
            require_relative "committee/request_validator"
         | 
| 8 8 | 
             
            require_relative "committee/response_generator"
         | 
| 9 9 | 
             
            require_relative "committee/response_validator"
         | 
| 10 10 | 
             
            require_relative "committee/router"
         | 
| 11 | 
            -
            require_relative "committee/schema"
         | 
| 12 11 |  | 
| 13 12 | 
             
            require_relative "committee/middleware/base"
         | 
| 14 13 | 
             
            require_relative "committee/middleware/request_validation"
         | 
| @@ -16,3 +15,11 @@ require_relative "committee/middleware/response_validation" | |
| 16 15 | 
             
            require_relative "committee/middleware/stub"
         | 
| 17 16 |  | 
| 18 17 | 
             
            require_relative "committee/test/methods"
         | 
| 18 | 
            +
             | 
| 19 | 
            +
            module Committee
         | 
| 20 | 
            +
              def self.warn_deprecated(message)
         | 
| 21 | 
            +
                if !$VERBOSE.nil?
         | 
| 22 | 
            +
                  $stderr.puts(message)
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
              end
         | 
| 25 | 
            +
            end
         | 
| @@ -7,61 +7,31 @@ describe Committee::Middleware::RequestValidation do | |
| 7 7 | 
             
                @app
         | 
| 8 8 | 
             
              end
         | 
| 9 9 |  | 
| 10 | 
            -
              it "detects an invalid Content-Type" do
         | 
| 11 | 
            -
                @app = new_rack_app
         | 
| 12 | 
            -
                header "Content-Type", "application/whats-this"
         | 
| 13 | 
            -
                post "/account/app-transfers", "{}"
         | 
| 14 | 
            -
                assert_equal 400, last_response.status
         | 
| 15 | 
            -
              end
         | 
| 16 | 
            -
             | 
| 17 10 | 
             
              it "passes through a valid request" do
         | 
| 18 11 | 
             
                @app = new_rack_app
         | 
| 19 12 | 
             
                params = {
         | 
| 20 | 
            -
                  " | 
| 21 | 
            -
                  "recipient" => "owner@heroku.com",
         | 
| 13 | 
            +
                  "name" => "cloudnasium"
         | 
| 22 14 | 
             
                }
         | 
| 23 15 | 
             
                header "Content-Type", "application/json"
         | 
| 24 | 
            -
                post "/ | 
| 16 | 
            +
                post "/apps", MultiJson.encode(params)
         | 
| 25 17 | 
             
                assert_equal 200, last_response.status
         | 
| 26 18 | 
             
              end
         | 
| 27 19 |  | 
| 28 | 
            -
              it "detects  | 
| 29 | 
            -
                @app = new_rack_app
         | 
| 30 | 
            -
                header "Content-Type", "application/json"
         | 
| 31 | 
            -
                post "/account/app-transfers", "{}"
         | 
| 32 | 
            -
                assert_equal 422, last_response.status
         | 
| 33 | 
            -
                assert_match /require params/i, last_response.body
         | 
| 34 | 
            -
              end
         | 
| 35 | 
            -
             | 
| 36 | 
            -
              it "detects an extra parameter" do
         | 
| 20 | 
            +
              it "detects an invalid Content-Type" do
         | 
| 37 21 | 
             
                @app = new_rack_app
         | 
| 22 | 
            +
                header "Content-Type", "application/whats-this"
         | 
| 38 23 | 
             
                params = {
         | 
| 39 | 
            -
                  " | 
| 40 | 
            -
                  "cloud" => "production",
         | 
| 41 | 
            -
                  "recipient" => "owner@heroku.com",
         | 
| 42 | 
            -
                }
         | 
| 43 | 
            -
                header "Content-Type", "application/json"
         | 
| 44 | 
            -
                post "/account/app-transfers", MultiJson.encode(params)
         | 
| 45 | 
            -
                assert_equal 422, last_response.status
         | 
| 46 | 
            -
                assert_match /unknown params/i, last_response.body
         | 
| 47 | 
            -
              end
         | 
| 48 | 
            -
             | 
| 49 | 
            -
              it "doesn't error on an extra parameter with allow_extra" do
         | 
| 50 | 
            -
                @app = new_rack_app(allow_extra: true)
         | 
| 51 | 
            -
                params = {
         | 
| 52 | 
            -
                  "app" => "heroku-api",
         | 
| 53 | 
            -
                  "cloud" => "production",
         | 
| 54 | 
            -
                  "recipient" => "owner@heroku.com",
         | 
| 24 | 
            +
                  "name" => "cloudnasium"
         | 
| 55 25 | 
             
                }
         | 
| 56 | 
            -
                 | 
| 57 | 
            -
                 | 
| 58 | 
            -
                 | 
| 26 | 
            +
                post "/apps", MultiJson.encode(params)
         | 
| 27 | 
            +
                assert_equal 400, last_response.status
         | 
| 28 | 
            +
                assert_match /unsupported content-type/i, last_response.body
         | 
| 59 29 | 
             
              end
         | 
| 60 30 |  | 
| 61 31 | 
             
              it "rescues JSON errors" do
         | 
| 62 32 | 
             
                @app = new_rack_app
         | 
| 63 33 | 
             
                header "Content-Type", "application/json"
         | 
| 64 | 
            -
                post "/ | 
| 34 | 
            +
                post "/apps", "{x:y}"
         | 
| 65 35 | 
             
                assert_equal 400, last_response.status
         | 
| 66 36 | 
             
                assert_match /valid json/i, last_response.body
         | 
| 67 37 | 
             
              end
         | 
| @@ -69,11 +39,21 @@ describe Committee::Middleware::RequestValidation do | |
| 69 39 | 
             
              it "takes a prefix" do
         | 
| 70 40 | 
             
                @app = new_rack_app(prefix: "/v1")
         | 
| 71 41 | 
             
                params = {
         | 
| 72 | 
            -
                  " | 
| 73 | 
            -
             | 
| 42 | 
            +
                  "name" => "cloudnasium"
         | 
| 43 | 
            +
                }
         | 
| 44 | 
            +
                header "Content-Type", "application/json"
         | 
| 45 | 
            +
                post "/v1/apps", MultiJson.encode(params)
         | 
| 46 | 
            +
                assert_equal 200, last_response.status
         | 
| 47 | 
            +
              end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
              it "warns when sending a deprecated string" do
         | 
| 50 | 
            +
                mock(Committee).warn_deprecated.with_any_args
         | 
| 51 | 
            +
                @app = new_rack_app(schema: File.read("./test/data/schema.json"))
         | 
| 52 | 
            +
                params = {
         | 
| 53 | 
            +
                  "name" => "cloudnasium"
         | 
| 74 54 | 
             
                }
         | 
| 75 55 | 
             
                header "Content-Type", "application/json"
         | 
| 76 | 
            -
                post "/ | 
| 56 | 
            +
                post "/apps", MultiJson.encode(params)
         | 
| 77 57 | 
             
                assert_equal 200, last_response.status
         | 
| 78 58 | 
             
              end
         | 
| 79 59 |  | 
| @@ -81,7 +61,7 @@ describe Committee::Middleware::RequestValidation do | |
| 81 61 |  | 
| 82 62 | 
             
              def new_rack_app(options = {})
         | 
| 83 63 | 
             
                options = {
         | 
| 84 | 
            -
                  schema: File.read("./test/data/schema.json")
         | 
| 64 | 
            +
                  schema: MultiJson.decode(File.read("./test/data/schema.json"))
         | 
| 85 65 | 
             
                }.merge(options)
         | 
| 86 66 | 
             
                Rack::Builder.new {
         | 
| 87 67 | 
             
                  use Committee::Middleware::RequestValidation, options
         | 
| @@ -13,14 +13,6 @@ describe Committee::Middleware::ResponseValidation do | |
| 13 13 | 
             
                assert_equal 200, last_response.status
         | 
| 14 14 | 
             
              end
         | 
| 15 15 |  | 
| 16 | 
            -
              it "detects an invalid response Content-Type" do
         | 
| 17 | 
            -
                @app = new_rack_app(MultiJson.encode([ValidApp]),
         | 
| 18 | 
            -
                  { "Content-Type" => "application/xml" })
         | 
| 19 | 
            -
                get "/apps"
         | 
| 20 | 
            -
                assert_equal 500, last_response.status
         | 
| 21 | 
            -
                assert_match /response header must be set to/i, last_response.body
         | 
| 22 | 
            -
              end
         | 
| 23 | 
            -
             | 
| 24 16 | 
             
              it "detects an invalid response" do
         | 
| 25 17 | 
             
                @app = new_rack_app("")
         | 
| 26 18 | 
             
                get "/apps"
         | 
| @@ -28,24 +20,6 @@ describe Committee::Middleware::ResponseValidation do | |
| 28 20 | 
             
                assert_match /valid JSON/i, last_response.body
         | 
| 29 21 | 
             
              end
         | 
| 30 22 |  | 
| 31 | 
            -
              it "detects missing keys in response" do
         | 
| 32 | 
            -
                data = ValidApp.dup
         | 
| 33 | 
            -
                data.delete("name")
         | 
| 34 | 
            -
                @app = new_rack_app(MultiJson.encode([data]))
         | 
| 35 | 
            -
                get "/apps"
         | 
| 36 | 
            -
                assert_equal 500, last_response.status
         | 
| 37 | 
            -
                assert_match /missing keys/i, last_response.body
         | 
| 38 | 
            -
              end
         | 
| 39 | 
            -
             | 
| 40 | 
            -
              it "detects extra keys in response" do
         | 
| 41 | 
            -
                data = ValidApp.dup
         | 
| 42 | 
            -
                data.merge!("tier" => "important")
         | 
| 43 | 
            -
                @app = new_rack_app(MultiJson.encode([data]))
         | 
| 44 | 
            -
                get "/apps"
         | 
| 45 | 
            -
                assert_equal 500, last_response.status
         | 
| 46 | 
            -
                assert_match /extra keys/i, last_response.body
         | 
| 47 | 
            -
              end
         | 
| 48 | 
            -
             | 
| 49 23 | 
             
              it "rescues JSON errors" do
         | 
| 50 24 | 
             
                @app = new_rack_app("[{x:y}]")
         | 
| 51 25 | 
             
                get "/apps"
         | 
| @@ -59,6 +33,14 @@ describe Committee::Middleware::ResponseValidation do | |
| 59 33 | 
             
                assert_equal 200, last_response.status
         | 
| 60 34 | 
             
              end
         | 
| 61 35 |  | 
| 36 | 
            +
              it "warns when sending a deprecated string" do
         | 
| 37 | 
            +
                mock(Committee).warn_deprecated.with_any_args
         | 
| 38 | 
            +
                @app = new_rack_app(MultiJson.encode([ValidApp]), {},
         | 
| 39 | 
            +
                  schema: File.read("./test/data/schema.json"))
         | 
| 40 | 
            +
                get "/apps"
         | 
| 41 | 
            +
                assert_equal 200, last_response.status
         | 
| 42 | 
            +
              end
         | 
| 43 | 
            +
             | 
| 62 44 | 
             
              private
         | 
| 63 45 |  | 
| 64 46 | 
             
              def new_rack_app(response, headers = {}, options = {})
         | 
| @@ -66,7 +48,7 @@ describe Committee::Middleware::ResponseValidation do | |
| 66 48 | 
             
                  "Content-Type" => "application/json"
         | 
| 67 49 | 
             
                }.merge(headers)
         | 
| 68 50 | 
             
                options = {
         | 
| 69 | 
            -
                  schema: File.read("./test/data/schema.json")
         | 
| 51 | 
            +
                  schema: MultiJson.decode(File.read("./test/data/schema.json"))
         | 
| 70 52 | 
             
                }.merge(options)
         | 
| 71 53 | 
             
                Rack::Builder.new {
         | 
| 72 54 | 
             
                  use Committee::Middleware::ResponseValidation, options
         | 
| @@ -46,12 +46,21 @@ describe Committee::Middleware::Stub do | |
| 46 46 | 
             
                assert_equal ValidApp.keys.sort, data.keys.sort
         | 
| 47 47 | 
             
              end
         | 
| 48 48 |  | 
| 49 | 
            +
              it "warns when sending a deprecated string" do
         | 
| 50 | 
            +
                mock(Committee).warn_deprecated.with_any_args
         | 
| 51 | 
            +
                @app = new_rack_app(schema: File.read("./test/data/schema.json"))
         | 
| 52 | 
            +
                get "/apps/heroku-api"
         | 
| 53 | 
            +
                assert_equal 200, last_response.status
         | 
| 54 | 
            +
                data = MultiJson.decode(last_response.body)
         | 
| 55 | 
            +
                assert_equal ValidApp.keys.sort, data.keys.sort
         | 
| 56 | 
            +
              end
         | 
| 57 | 
            +
             | 
| 49 58 | 
             
              private
         | 
| 50 59 |  | 
| 51 60 | 
             
              def new_rack_app(options = {})
         | 
| 52 61 | 
             
                suppress = options.delete(:suppress)
         | 
| 53 62 | 
             
                options = {
         | 
| 54 | 
            -
                  schema: File.read("./test/data/schema.json")
         | 
| 63 | 
            +
                  schema: MultiJson.decode(File.read("./test/data/schema.json"))
         | 
| 55 64 | 
             
                }.merge(options)
         | 
| 56 65 | 
             
                Rack::Builder.new {
         | 
| 57 66 | 
             
                  use Committee::Middleware::Stub, options
         |