Skip to content

Commit

Permalink
chore: copy problem+json decorator for Dry::MessageSet back from pact…
Browse files Browse the repository at this point in the history
…flow
  • Loading branch information
bethesque committed Sep 15, 2023
1 parent 55bb62c commit 6e3d4af
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 42 deletions.
2 changes: 1 addition & 1 deletion lib/pact_broker/api/decorators/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def class_for(name)
def validation_error_decorator_class_for(errors_class, accept_header)
if accept_header&.include?("application/problem+json")
if errors_class == Dry::Validation::MessageSet
PactBroker::Api::Decorators::DryValidationErrorsProblemJSONDecorator
PactBroker::Api::Decorators::DryValidationErrorsProblemJsonDecorator
else
PactBroker::Api::Decorators::ValidationErrorsProblemJSONDecorator
end
Expand Down
Original file line number Diff line number Diff line change
@@ -1,44 +1,23 @@
require "pact_broker/api/decorators/base_decorator"
require "pact_broker/api/decorators/embedded_error_problem_json_decorator"
# Formats a Dry::Validation::MessageSet into application/problem+json format.
# according to the spec at https://www.rfc-editor.org/rfc/rfc9457.html

# Decorates Dry::Validation::MessageSet
# Defaults to displaying validation errors, but the top level
# details may be overridden to display error responses for other HTTP statuses (eg. 409)
module PactBroker
module Api
module Decorators
class DryValidationErrorsProblemJSONDecorator
# @param errors [Dry::Validation::MessageSet]
def initialize(errors)
@errors = errors
end
class DryValidationErrorsProblemJsonDecorator < BaseDecorator

# @return [Hash]
def to_hash(user_options:, **)
error_list = errors.collect{ |e| error_hash(e, user_options[:base_url]) }
{
"title" => "Validation errors",
"type" => "#{user_options[:base_url]}/problems/validation-error",
"status" => 400,
"instance" => "/",
"errors" => error_list
}
end
property :title, getter: -> (user_options:, **) { user_options[:title] || "Validation errors" }
property :type, getter: -> (user_options:, **) { user_options[:type] || "#{user_options[:base_url]}/problems/validation-error" }
property :detail, getter: -> (user_options:, **) { user_options[:detail] || nil }
property :status, getter: -> (user_options:, **) { user_options[:status] || 400 }
property :instance, getter: -> (user_options:, **) { user_options[:instance] || "/" }

# @return [String] JSON
def to_json(*args, **kwargs)
to_hash(*args, **kwargs).to_json
end

private

attr_reader :errors

def error_hash(error, base_url)
{
"type" => "#{base_url}/problems/invalid-body-property-value",
"title" => "Validation error",
"detail" => error.text,
"pointer" => "/" + error.path.join("/"),
"status" => 400
}
end
collection :entries, as: :errors, extend: PactBroker::Api::Decorators::EmbeddedErrorProblemJsonDecorator
end
end
end
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
require "pact_broker/api/decorators/base_decorator"

# Decorates the individual validation error message Dry::Validation::Message

module PactBroker
module Api
module Decorators
class EmbeddedErrorProblemJsonDecorator < BaseDecorator

property :type, getter: -> (decorator:, user_options:, **) { decorator.type(user_options[:base_url]) }
property :title, exec_context: :decorator
property :text, as: :detail
property :pointer, exec_context: :decorator
property :parameter, exec_context: :decorator
property :status, getter: -> (user_options:, **) { user_options[:status] || 400 }

# dry-validation doesn't support validating a top level array, so we wrap
# the json patch operations array in a hash with the key :_ops to validate it.
# When we render the error, we have to remove the /_ops prefix from the pointer.
# For contracts where we validate the path and the body together using _path and _body
# we also need to remove the first key from the path.
# It's possible the pointer should have a # at the start of it as per https://www.rfc-editor.org/rfc/rfc6901 :shrug:
def pointer
if is_path_error?
nil
# _ops, _path or _body for use when we need to hack the way dry-validation schemas work
elsif represented.path.first.to_s.start_with?("_")
"/" + represented.path[1..-1].join("/")
else
"/" + represented.path.join("/")
end
end

def parameter
if is_path_error?
represented.path.last.to_s
else
nil
end
end

def title
if is_path_error?
"Invalid path segment"
else
"Invalid body parameter"
end
end

# @param [String] base_url
def type(base_url)
if is_path_error?
"#{base_url}/#{path_type}"
else
"#{base_url}/#{body_type}"
end
end

def path_type
if represented.text.include?("missing")
"problems/missing-request-parameter"
elsif represented.text.include?("format")
"problems/invalid-request-parameter-format"
else
"problems/invalid-request-parameter-value"
end
end

def body_type
if represented.text.include?("missing")
"problems/missing-body-property"
elsif represented.text.include?("format")
"problems/invalid-body-property-format"
else
"problems/invalid-body-property-value"
end
end

def is_path_error?
represented.path.first == :_path
end
end
end
end
end
2 changes: 1 addition & 1 deletion spec/lib/pact_broker/api/decorators/configuration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ module Decorators
let(:errors_class) { Dry::Validation::MessageSet }
let(:accept_header) { "application/hal+json, application/problem+json" }

it { is_expected.to be PactBroker::Api::Decorators::DryValidationErrorsProblemJSONDecorator }
it { is_expected.to be PactBroker::Api::Decorators::DryValidationErrorsProblemJsonDecorator }
end

context "when given Hash and application/hal+json, application/problem+json" do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@
module PactBroker
module Api
module Decorators
describe DryValidationErrorsProblemJSONDecorator do
describe DryValidationErrorsProblemJsonDecorator do
describe "#to_json" do

class TestContract < PactBroker::Api::Contracts::BaseContract
json do
optional(:foo).maybe(:hash) do
Expand All @@ -17,9 +16,9 @@ class TestContract < PactBroker::Api::Contracts::BaseContract

let(:decorator_options) { { user_options: { base_url: "http://example.org" } } }

subject { DryValidationErrorsProblemJSONDecorator.new(validation_errors).to_hash(**decorator_options) }
subject { DryValidationErrorsProblemJsonDecorator.new(validation_errors).to_hash(**decorator_options) }

context "with a hash of errors" do
context "with a MessageSet of errors" do
let(:validation_errors) do
TestContract.new.call({ foo: { bar: 1 }}).errors
end
Expand All @@ -35,15 +34,35 @@ class TestContract < PactBroker::Api::Contracts::BaseContract
{
"type" => "http://example.org/problems/invalid-body-property-value",
"pointer" => "/foo/bar",
"title" => "Validation error",
"title" => "Invalid body parameter",
"detail" => "must be a string",
"status" => 400
}
]
}
end

it { is_expected.to match_pact(expected_hash, allow_unexpected_keys: false)}
it { is_expected.to match_pact(expected_hash, allow_unexpected_keys: false) }
end

context "when the top level details are customised via user_options" do
let(:decorator_options) { { user_options: { title: "title", type: "type", detail: "detail", status: 409, instance: "/foo" } } }

let(:expected_hash) do
{
"title" => "title",
"type" => "type",
"status" => 409,
"instance" => "/foo",
"detail" => "detail"
}
end

let(:validation_errors) do
TestContract.new.call({ foo: { bar: 1 }}).errors
end

it { is_expected.to match_pact(expected_hash, allow_unexpected_keys: true) }
end
end
end
Expand Down

0 comments on commit 6e3d4af

Please sign in to comment.