-
-
Notifications
You must be signed in to change notification settings - Fork 178
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: upgrade dry-validation gem in preparation for ruby 3 upgrade
- Loading branch information
Showing
70 changed files
with
1,271 additions
and
1,160 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
Dir.glob(File.expand_path(File.join(__FILE__, "..", "contracts", "*.rb"))).sort.each do | path | | ||
require path | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
# Using dry-validation | ||
|
||
## Things you need to know about dry-validation | ||
|
||
* It's better than it used to be. | ||
* There are two parts to a Dry Validation contract. | ||
* Schemas: are for defining TYPES and presence of values. They come in 3 different flavours - json, string params, and vanilla https://dry-rb.org/gems/dry-validation/1.8/schemas/. We generally use the JSON one. | ||
* Rules: are for applying VALIDATION RULES. Don't try and put validation rules in the schema, or you'll end up wanting to punch something. | ||
* The schema is applied first, and only if it passes are the rules evaluated for that particular parameter. | ||
* The rules do NOT stop evaluating when one of them fails. Beth thinks this is less than ideal. | ||
* Macros allow you to apply rules to fields in a declarative way. https://dry-rb.org/gems/dry-validation/1.8/macros/ We have some [here](lib/pact_broker/api/contracts/dry_validation_macros.rb) | ||
* The docs are brief, but worth skimming through | ||
* https://dry-rb.org/gems/dry-validation/1.8/ | ||
* https://dry-rb.org/gems/dry-schema/1.10/ | ||
* You can include modules and define methods inside the Contract classes. | ||
* the "filled?" predicate in dry-schema will fail for "" but pass for " " | ||
|
||
## dry-validation modifications for Pact Broker | ||
|
||
* I cannot find any native support for Contract composition (there's schema composition, but I can't find Contract composition). The macros `validate_with_contract` and `validate_each_with_contract` in lib/pact_broker/api/contracts/dry_validation_macros.rb allow a child item or array of child items to be validated by a contract that is defined in a separate file, merging the results into the parent's results. There's an example in lib/pact_broker/api/contracts/pacts_for_verification_json_query_schema.rb | ||
* The format that dry-validation returns the error messages for arrays didn't work with the original Pact Broker error format, so the errors are formatted in lib/pact_broker/api/contracts/dry_validation_errors_formatter.rb (one day, the errors should be returned directly to the resource, and the error decorator can either format them in problem json, or raw hash) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
34 changes: 34 additions & 0 deletions
34
lib/pact_broker/api/contracts/can_i_deploy_query_schema.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
require "dry-validation" | ||
require "pact_broker/messages" | ||
require "pact_broker/project_root" | ||
require "pact_broker/string_refinements" | ||
|
||
module PactBroker | ||
module Api | ||
module Contracts | ||
class CanIDeployQuerySchema < BaseContract | ||
using PactBroker::StringRefinements | ||
|
||
params do | ||
required(:pacticipant).filled(:string) | ||
required(:version).filled(:string) | ||
optional(:to).filled(:string) | ||
optional(:environment).filled(:string) | ||
end | ||
|
||
rule(:pacticipant).validate(:pacticipant_with_name_exists) | ||
rule(:environment).validate(:environment_with_name_exists) | ||
|
||
rule(:to, :environment) do | ||
if provided?(values[:to]) && provided?(values[:environment]) | ||
base.failure(PactBroker::Messages.message("errors.validation.cannot_specify_tag_and_environment")) | ||
end | ||
|
||
if not_provided?(values[:to]) && not_provided?(values[:environment]) | ||
base.failure(PactBroker::Messages.message("errors.validation.must_specify_environment_or_tag")) | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end |
140 changes: 140 additions & 0 deletions
140
lib/pact_broker/api/contracts/consumer_version_selector_contract.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
require "pact_broker/api/contracts/base_contract" | ||
|
||
module PactBroker | ||
module Api | ||
module Contracts | ||
class ConsumerVersionSelectorContract < BaseContract | ||
option :parent # the parent hash in which the ConsumerVersionSelector is embedded | ||
|
||
BRANCH_KEYS = [:latest, :tag, :fallbackTag, :branch, :fallbackBranch, :matchingBranch, :mainBranch] | ||
ENVIRONMENT_KEYS = [:environment, :deployed, :released, :deployedOrReleased] | ||
ALL_KEYS = BRANCH_KEYS + ENVIRONMENT_KEYS + [:consumer] | ||
|
||
json do | ||
optional(:mainBranch).filled(included_in?: [true]) | ||
optional(:tag).filled(:str?) | ||
optional(:branch).filled(:str?) | ||
optional(:matchingBranch).filled(included_in?: [true]) | ||
optional(:latest).filled(included_in?: [true, false]) | ||
optional(:fallbackTag).filled(:str?) | ||
optional(:fallbackBranch).filled(:str?) | ||
optional(:consumer).filled(:str?) | ||
optional(:deployed).filled(included_in?: [true]) | ||
optional(:released).filled(included_in?: [true]) | ||
optional(:deployedOrReleased).filled(included_in?: [true]) | ||
optional(:environment).filled(:str?) | ||
end | ||
|
||
rule(:consumer).validate(:not_blank_if_present) | ||
|
||
# has minimum required params | ||
rule(*ALL_KEYS) do | ||
if not_provided?(values[:mainBranch]) && | ||
not_provided?(values[:tag]) && | ||
not_provided?(values[:branch]) && | ||
not_provided?(values[:environment]) && | ||
values[:matchingBranch] != true && | ||
values[:deployed] != true && | ||
values[:released] != true && | ||
values[:deployedOrReleased] != true && | ||
values[:latest] != true | ||
|
||
base.failure(validation_message("pacts_for_verification_selector_required_params_missing")) | ||
end | ||
end | ||
|
||
# mainBranch | ||
rule(*ALL_KEYS) do | ||
if values[:mainBranch] && values.slice(*ALL_KEYS - [:consumer, :mainBranch, :latest]).any? | ||
base.failure(validation_message("pacts_for_verification_selector_main_branch_with_other_param_disallowed")) | ||
end | ||
end | ||
|
||
# mainBranch/latest | ||
rule(:mainBranch, :latest) do | ||
if values[:mainBranch] && values[:latest] == false | ||
base.failure(validation_message("pacts_for_verification_selector_main_branch_and_latest_false_disallowed")) | ||
end | ||
end | ||
|
||
# matchingBranch | ||
rule(*ALL_KEYS) do | ||
if values[:matchingBranch] && values.slice(*ALL_KEYS - [:consumer, :matchingBranch]).any? | ||
base.failure(validation_message("pacts_for_verification_selector_matching_branch_with_other_param_disallowed")) | ||
end | ||
|
||
if values[:matchingBranch] && not_provided?(parent[:providerVersionBranch]) | ||
base.failure(validation_message("pacts_for_verification_selector_matching_branch_requires_provider_version_branch")) | ||
end | ||
end | ||
|
||
# tag and branch | ||
rule(:tag, :branch) do | ||
if values[:tag] && values[:branch] | ||
base.failure(validation_message("pacts_for_verification_selector_tag_and_branch_disallowed")) | ||
end | ||
end | ||
|
||
# branch/environment keys | ||
rule(*ALL_KEYS) do | ||
non_environment_fields = values.slice(*BRANCH_KEYS).keys.sort | ||
environment_related_fields = values.slice(*ENVIRONMENT_KEYS).keys.sort | ||
|
||
if (non_environment_fields.any? && environment_related_fields.any?) | ||
base.failure("cannot specify the #{PactBroker::Messages.pluralize("field", non_environment_fields.count)} #{non_environment_fields.join("/")} with the #{PactBroker::Messages.pluralize("field", environment_related_fields.count)} #{environment_related_fields.join("/")}") | ||
end | ||
end | ||
|
||
# fallbackTag | ||
rule(:fallbackTag, :tag, :latest) do | ||
if values[:fallbackTag] && !values[:latest] | ||
base.failure(validation_message("pacts_for_verification_selector_fallback_tag")) | ||
end | ||
|
||
if values[:fallbackTag] && !values[:tag] | ||
base.failure(validation_message("pacts_for_verification_selector_fallback_tag_requires_tag")) | ||
end | ||
end | ||
|
||
# fallbackBranch | ||
rule(:fallbackBranch, :branch, :latest) do | ||
if values[:fallbackBranch] && !values[:branch] | ||
base.failure(validation_message("pacts_for_verification_selector_fallback_branch_requires_branch")) | ||
end | ||
|
||
|
||
if values[:fallbackBranch] && values[:latest] == false | ||
base.failure(validation_message("pacts_for_verification_selector_fallback_branch_and_latest_false_disallowed")) | ||
end | ||
end | ||
|
||
# branch/latest | ||
rule(:branch, :latest) do | ||
if values[:branch] && values[:latest] == false | ||
base.failure(validation_message("pacts_for_verification_selector_branch_and_latest_false_disallowed")) | ||
end | ||
end | ||
|
||
# environment | ||
rule(:environment) do | ||
validate_environment_with_name_exists(value, key) if provided?(value) | ||
end | ||
|
||
# deployed, released, deployedOrReleased | ||
rule(:deployed, :released, :deployedOrReleased) do | ||
if values[:deployed] && values[:released] | ||
base.failure(validation_message("pacts_for_verification_selector_deployed_and_released_disallowed")) | ||
end | ||
|
||
if values[:deployed] && values[:deployedOrReleased] | ||
base.failure(validation_message("pacts_for_verification_selector_deployed_and_deployed_or_released_disallowed")) | ||
end | ||
|
||
if values[:released] && values[:deployedOrReleased] | ||
base.failure(validation_message("pacts_for_verification_selector_released_and_deployed_or_released_disallowed")) | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end |
50 changes: 50 additions & 0 deletions
50
lib/pact_broker/api/contracts/dry_validation_errors_formatter.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
require "pact_broker/error" | ||
|
||
module PactBroker | ||
module Api | ||
module Contracts | ||
module DryValidationErrorsFormatter | ||
|
||
# Formats the dry validation errors in the expected PactBroker error format of { :key => ["errors"] } | ||
# where there are no nested hashes. | ||
# @param [Dry::Validation::MessageSet] errors | ||
# @return [Hash] | ||
def format_errors(errors) | ||
errors.each_with_object({}) do | error, errors_hash | | ||
integers = error.path.select{ | k | k.is_a?(Integer) } | ||
|
||
if integers.size > 1 | ||
raise PactBroker::Error, "Cannot currently format an error message with more than one index" | ||
end | ||
|
||
if integers.empty? | ||
key = error.path == [nil] ? nil : error.path.join(".").to_sym | ||
add_error(errors_hash, key, error.text) | ||
else | ||
add_indexed_error(errors_hash, error) | ||
end | ||
end | ||
end | ||
|
||
# @private | ||
def add_error(errors_hash, key, text) | ||
errors_hash[key] ||= [] | ||
errors_hash[key] << text | ||
end | ||
|
||
# @private | ||
def add_indexed_error(errors_hash, error) | ||
error_path_classes = error.path.collect(&:class) | ||
if error_path_classes == [Symbol, Integer, Symbol] | ||
add_error(errors_hash, error.path.first, "#{error.path.last} #{error.text} (at index #{error.path[1]})") | ||
elsif error_path_classes == [Symbol, Integer] | ||
add_error(errors_hash, error.path.first, "#{error.text} (at index #{error.path[1]})") | ||
else | ||
# Don't have any usecases for this - will deal with it when it happens | ||
raise PactBroker::Error, "Cannot currently format an error message with path classes #{error_path_classes}" | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
require "pact_broker/api/contracts/dry_validation_methods" | ||
|
||
Dry::Validation.register_macro(:not_multiple_lines) do | ||
PactBroker::Api::Contracts::DryValidationMethods.validate_not_multiple_lines(value, key) | ||
end | ||
|
||
Dry::Validation.register_macro(:no_spaces_if_present) do | ||
PactBroker::Api::Contracts::DryValidationMethods.validate_no_spaces_if_present(value, key) | ||
end | ||
|
||
Dry::Validation.register_macro(:not_blank_if_present) do | ||
PactBroker::Api::Contracts::DryValidationMethods.validate_not_blank_if_present(value, key) | ||
end | ||
|
||
Dry::Validation.register_macro(:array_values_not_blank_if_any) do | ||
value&.each_with_index do | item, index | | ||
PactBroker::Api::Contracts::DryValidationMethods.validate_not_blank_if_present(item, key(path.keys + [index])) | ||
end | ||
end | ||
|
||
Dry::Validation.register_macro(:valid_url_if_present) do | ||
PactBroker::Api::Contracts::DryValidationMethods.validate_valid_url(value, key) | ||
end | ||
|
||
Dry::Validation.register_macro(:valid_version_number) do | ||
PactBroker::Api::Contracts::DryValidationMethods.validate_version_number(value, key) | ||
end | ||
|
||
Dry::Validation.register_macro(:pacticipant_with_name_exists) do | ||
PactBroker::Api::Contracts::DryValidationMethods.validate_pacticipant_with_name_exists(value, key) | ||
end | ||
|
||
Dry::Validation.register_macro(:environment_with_name_exists) do | ||
PactBroker::Api::Contracts::DryValidationMethods.validate_environment_with_name_exists(value, key) | ||
end | ||
|
||
# Validate each object in an array with the specified contract, | ||
# and merge the errors into the appropriate path in the parent | ||
# validation results. | ||
# eg. | ||
# rule(:myChildArray).validate(validate_each_with_contract: MyChildContract) | ||
# | ||
# If the child contract defines a option called `parent` then it can access the parent | ||
# hash for validation rules that need to work across the levels. | ||
# eg. ConsumerVersionSelectorContract for the matchingBranch rule | ||
# | ||
# Will not add any errors if the array is nil | ||
Dry::Validation.register_macro(:validate_each_with_contract) do |macro:| | ||
value&.each_with_index do | item, index | | ||
child_contract_class = macro.args[0] | ||
messages = child_contract_class.new(parent: values).call(item).errors(full: true).to_hash.values.flatten | ||
messages.each do | message | | ||
key(path.keys + [index]).failure(message) | ||
end | ||
end | ||
end | ||
|
||
# Validate a child node with the specified contract, | ||
# and merge the errors into the appropriate path in the parent | ||
# validation results. | ||
# eg. | ||
# rule(:myChildHash).validate(validate_with_contract: MyChildContract) | ||
# | ||
# If the child contract defines a option called `parent` then it can access the parent | ||
# hash for validation rules that need to work across the levels. | ||
# eg. ConsumerVersionSelectorContract for the matchingBranch rule | ||
# | ||
# Will not add any errors if the value is nil | ||
Dry::Validation.register_macro(:validate_with_contract) do |macro:| | ||
if value | ||
child_contract_class = macro.args[0] | ||
errors = child_contract_class.new(parent: values).call(value).errors.to_hash | ||
errors.each do | key, messages | | ||
messages.each do | message | | ||
key(path.keys + [key]).failure(message) | ||
end | ||
end | ||
end | ||
end |
Oops, something went wrong.