Contracts play a vital role in representing user-entered data within an application. They act as objects that encapsulate information available for modification, such as data from HTML forms or API payloads. When passed to repositories for persistence operations, contracts serve as the provided data from the app user. They possess knowledge about the expected attributes within a payload and enforce constraints to determine the validity of their own state.
It is crucial to distinguish contracts from records. Unlike records, which represent domain entities, contracts purely represent data entered by the user. They do not possess numeric identifiers since those are typically generated by the system and not set by users. Furthermore, contracts do not have strong expectations regarding data integrity since user-entered data can vary in type and may even be absent altogether.
User input validation holds significant importance within the business logic of any application. It ensures that incoming data is sensible, correct, and adheres to a predefined schema. In a default Rails app, the responsibility of validation rules often falls upon record objects, adding to their already numerous responsibilities. However, it is beneficial to separate input validation from persistence operations and integrate it within the business logic layer. This is where contract objects come into play as an ideal solution. By leveraging validation utilities from library dry-validation, contracts can effectively handle input validation tasks.
Now, let’s consider making changes to the existing app code to incorporate contracts. One approach involves creating a Bike Contract that encompasses the necessary attributes, such as the bike’s name. These contracts will serve as the actual arguments for the create and update methods within the Bike Repository. Additionally, we will define validation rules within the contract to enforce data integrity and ensure the validity of user input.
require "dry/validation"
module Bikes
class Contract < Dry::Validation::Contract
params do
required(:name).filled(:string)
end
end
end
def create(request)
bike_data = Bikes::Contract.new.call(JSON.parse(request.body.read))
if bike_data.errors.to_h.any?
[500, {"content-type" => "text/plain"}, ["Error creating bike"]]
else
bike = Bikes::Repository.new.create(name: bike_data["name"])
if bike
[201, {"content-type" => "text/plain"}, ["Create"]]
else
[500, {"content-type" => "text/plain"}, ["Error creating bike"]]
end
end
end
RSpec.describe Bikes::Contract do
subject(:contract) { described_class.new }
describe "validation rules" do
context "when all required attributes are present" do
let(:valid_attributes) { { name: "Mountain Bike" } }
it "passes validation" do
expect(contract.call(valid_attributes)).to be_success
end
end
context "when required attributes are missing" do
let(:invalid_attributes) { { name: "" } }
it "fails validation" do
expect(contract.call(invalid_attributes)).to be_failure
end
it "includes an error message for the missing attribute" do
result = contract.call(invalid_attributes)
expect(result.errors.to_h).to include(name: ["must be filled"])
end
end
end
end
By adopting contracts, we enhance the clarity and separation of concerns within our codebase. Contracts become responsible for validating user input, freeing the record objects from this additional responsibility. As a result, the code becomes more modular, easier to maintain, and promotes better adherence to the single responsibility principle.
In summary, contracts play a crucial role in representing user-entered data within an application. They provide a means to validate and enforce constraints on incoming data, ensuring its sanity and adherence to predefined schemas. By leveraging contracts in our app code, we can separate the responsibilities of validation and persistence, leading to improved code organization, maintainability, and adherence to software design principles. The whole code where I added contracts into the app is on github