Mutations Ruby Library

repository·master·Indexed 23 days ago

https://github.com/cypriss/mutations

A Ruby library for composing business logic into discrete, command-like objects that handle input sanitization, validation, and execution. Designed for building maintainable service layers in Rails applications and JSON APIs, it allows developers to define input schemas using required and optional blocks and execute logic via Mutations::Command.

Tokens
5K
Snippets
7
Records
30
Agent score
80%

What's inside mutations

  1. Define a Mutation Command

    master

    To create a new business logic operation, subclass Mutations::Command. You define your input schema using required and optional blocks, and implement the core logic in the execute method. The execute method is only called if all input validations pass.

    class UserSignup < Mutations::Command
    
      # Required inputs
      required do
        string :email, matches: EMAIL_REGEX
        string :name
      end
    
      # Optional inputs
      optional do
        boolean :newsletter_subscribe
      end
    
      # Business logic
      def execute
        user = User.create!(inputs)
        NewsletterSubscriptions.create(email: email, user_id: user.id) if newsletter_subscribe
        UserMailer.async(:deliver_welcome, user.id)
        user
      end
    end
  2. Use required and optional blocks in HashFilter

    master

    When defining a HashFilter schema, you use blocks to specify the scope of the keys being defined:

    • required(&block): Defines keys that must be present in the input data. If a required key is missing, the filter will return a :required error.
    • optional(&block): Defines keys that may or may not be present. If an optional key is missing, it is simply ignored (unless it has a default value).

    Switching between these blocks changes the internal @current_inputs pointer used by the DSL.

  3. Define a business logic command by subclassing Mutations::Command

    master

    To implement a business action, create a class that inherits from Mutations::Command. You must define which inputs are required and which are optional using the class methods provided. You should also override the execute method to contain your business logic and optionally override the validate method for custom validation logic.

    Inputs defined via required or optional are automatically accessible via getter and setter methods on the command instance.

  4. Run a mutation and handle outcomes

    master

    You can execute a mutation in two ways:

    1. Using .run(params): Returns a Mutations::Outcome object. Use this when you want to check for success or failure without raising exceptions.
    2. Using .run!(params): Returns the result of the execute method directly, or raises a Mutations::ValidationException if validation fails.

    Example using .run:

    outcome = UserSignup.run(params[:user])
    
    if outcome.success?
      # Access the result of the execute method
      user = outcome.result
      render json: {message: "Great success, #{user.name}!"}
    else
      # Access error details
      render json: outcome.errors.symbolic, status: 422
    end

    Example using .run!:

    user = UserSignup.run!(params)
    # Using .run
    outcome = UserSignup.run(params[:user])
    
    if outcome.success?
      render json: {message: "Great success, #{outcome.result.name}!"}
    else
      render json: outcome.errors.symbolic, status: 422
    end
    
    # Using .run!
    user = UserSignup.run!(params)
  5. Access inputs and add custom errors in execute

    master

    Inside the execute method, you can access inputs via self.inputs (a hash with indifferent access) or via helper methods named after the input keys.

    Input helpers:

    • self.email: Returns the value of the email input.
    • self.email=(val): Allows setting the value (rarely used).
    • self.email_present?: Returns true if the input was provided (useful for optional inputs).

    Adding errors: You can add errors manually inside the execute method or a validate method using add_error(key, symbolic_error, message).

    # Inside execute
    def execute
      if email =~ /aol.com/
        add_error(:email, :old_school, "Wow, you still use AOL?")
        return
      end
    end
    
    # Inside validate (prevents execute from running)
    def validate
      if password != password_confirmation
        add_error(:password_confirmation, :doesnt_match, "Your passwords don't match")
      end
    end
  6. Pass multiple hashes to a mutation

    master

    The .run and .run! methods accept hashes as arguments. You can pass multiple hashes, which will be merged together. Later hashes take precedence over earlier ones. This is useful for merging unsafe user input with safe server-side data.

    # A user comments on an article
    class CreateComment < Mutations::Command
      required do
        model :user
        model :article
        string :comment, max_length: 500
      end
    
      def execute; ...; end
    end
    
    def somewhere
      # params[:comment] might contain a 'user' key, but it will be overwritten by the second hash
      outcome = CreateComment.run(params[:comment], 
        user: current_user, 
        article: Article.find(params[:article_id])
      )
    end
  7. Define input schemas with required and optional blocks

    master

    Inside a Mutations::Command subclass, use required and optional blocks to define the expected input types and validations. Supported types include string, symbol, integer, boolean, array, hash, model, and more.

    Required inputs:

    required do
      string :name, max_length: 10
      symbol :state, in: %i(AL AK AR ... WY)
      integer :age
      boolean :is_special, default: true
      model :account
    end

    Optional inputs:

    optional do
      array :tags, class: String
      hash :prefs do
        boolean :smoking
        boolean :view
      end
    end
  8. Configure the TimeFilter mutation

    master

    The Mutations::TimeFilter is used to validate and sanitize time-related inputs. It can coerce strings, dates, or date-times into Time objects and enforce range constraints.

    Available configuration options:

    • :nils: Boolean. If true, an explicit nil input is considered valid. Defaults to false.
    • :format: String. If provided, Time.strptime is used with this format for coercion. If nil, Time.parse is used. Defaults to nil.
    • :after: Time object. Represents the minimum allowed time (inclusive). If the input is earlier than or equal to this value, validation fails.
    • :before: Time object. Represents the maximum allowed time (inclusive). If the input is later than or equal to this value, validation fails.
  9. Configure the DateFilter mutation

    master

    The Mutations::DateFilter is used to validate and sanitize date inputs. It can be configured with several options to handle nil values, empty strings, specific date formats, and range constraints.

    When filter(data) is called, it returns an array containing [processed_value, error_code]. If the input is valid, the error code is nil. If invalid, the error code indicates the reason (e.g., :nils, :empty, :date, :after, or :before).

  10. Configure Mutations::ModelFilter options

    master

    The Mutations::ModelFilter is used to validate input data against specific model schemas or classes. When initializing a filter, you can provide an options hash to control how data is interpreted and validated.

    Available options:

    • :nils (Boolean): If true, an explicit nil value is considered valid. If false (default), nil returns an error code :nils.
    • :class (Class or String): The class that the input data must be an instance of. If not provided, it defaults to the name of the filter camelized and constantized (e.g., a filter named :user_profile defaults to UserProfile).
    • :builder (Class or String): A class used to construct the model from a Hash. If a :builder is present and the input data is a Hash, the builder's .run(data) method is called. If successful, the builder's result is used for subsequent validation.
    • :new_records (Boolean): Controls whether unsaved ActiveRecord-style records are allowed. If false (default), an object that responds to new_record? and returns true will be rejected with the error code :new_records. If true, any object of the correct class is valid.
    options = {
      :nils => false,
      :class => nil,
      :builder => nil,
      :new_records => false
    }
  11. Handle validation errors with Mutations::ErrorHash

    master

    When a mutation fails, the outcome contains a Mutations::ErrorHash object. This object provides several ways to access error information:

    • outcome.errors.symbolic: A hash mapping input keys to error symbols (e.g., {email: :required}).
    • outcome.errors.message: A hash mapping input keys to human-readable error messages (e.g., {email: "Email is required"}).
    • outcome.errors.message_list: An array of all error message strings.

    Example:

    # If inputs are: name: "Bob", newsletter_subscribe: "Wat"
    outcome = UserSignup.run(name: "Bob", newsletter_subscribe: "Wat")
    
    unless outcome.success?
      outcome.errors.symbolic   # => {email: :required, newsletter_subscribe: :boolean}
      outcome.errors.message    # => {email: "Email is required", newsletter_subscribe: "Newsletter Subscription isn't a boolean"}
      outcome.errors.message_list # => ["Email is required", "Newsletter Subscription isn't a boolean"]
    end