{"id":13482596,"url":"https://github.com/cypriss/mutations","last_synced_at":"2025-05-14T10:14:46.165Z","repository":{"id":5134297,"uuid":"6300586","full_name":"cypriss/mutations","owner":"cypriss","description":"Compose your business logic into commands that sanitize and validate input.","archived":false,"fork":false,"pushed_at":"2023-01-17T08:14:37.000Z","size":258,"stargazers_count":1396,"open_issues_count":30,"forks_count":93,"subscribers_count":29,"default_branch":"master","last_synced_at":"2025-05-13T23:54:01.605Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":"slacker/purifier","license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cypriss.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"MIT-LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2012-10-19T19:38:09.000Z","updated_at":"2025-04-17T11:00:45.000Z","dependencies_parsed_at":"2023-02-10T08:31:47.773Z","dependency_job_id":null,"html_url":"https://github.com/cypriss/mutations","commit_stats":null,"previous_names":[],"tags_count":23,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cypriss%2Fmutations","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cypriss%2Fmutations/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cypriss%2Fmutations/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cypriss%2Fmutations/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cypriss","download_url":"https://codeload.github.com/cypriss/mutations/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254120179,"owners_count":22017953,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-07-31T17:01:03.609Z","updated_at":"2025-05-14T10:14:46.117Z","avatar_url":"https://github.com/cypriss.png","language":"Ruby","readme":"# Mutations\n\n[![Build Status](https://travis-ci.org/cypriss/mutations.svg?branch=master)](https://travis-ci.org/cypriss/mutations)\n[![Code Climate](https://codeclimate.com/github/cypriss/mutations.svg)](https://codeclimate.com/github/cypriss/mutations)\n\nCompose your business logic into commands that sanitize and validate input. Write safe, reusable, and maintainable code for Ruby and Rails apps.\n\n## Installation\n\n    gem install mutations\n\nOr add it to your Gemfile:\n\n    gem 'mutations'\n\n## Example\n\n```ruby\n# Define a command that signs up a user.\nclass UserSignup \u003c Mutations::Command\n\n  # These inputs are required\n  required do\n    string :email, matches: EMAIL_REGEX\n    string :name\n  end\n\n  # These inputs are optional\n  optional do\n    boolean :newsletter_subscribe\n  end\n\n  # The execute method is called only if the inputs validate. It does your business action.\n  def execute\n    user = User.create!(inputs)\n    NewsletterSubscriptions.create(email: email, user_id: user.id) if newsletter_subscribe\n    UserMailer.async(:deliver_welcome, user.id)\n    user\n  end\nend\n\n# In a controller action (for instance), you can run it:\ndef create\n  outcome = UserSignup.run(params[:user])\n\n  # Then check to see if it worked:\n  if outcome.success?\n    render json: {message: \"Great success, #{outcome.result.name}!\"}\n  else\n    render json: outcome.errors.symbolic, status: 422\n  end\nend\n```\n\nSome things to note about the example:\n\n* We don't need attr_accessible or strong_attributes to protect against mass assignment attacks\n* We're guaranteed that within execute, the inputs will be the correct data types, even if they needed some coercion (all strings are stripped by default, and strings like \"1\" / \"0\" are converted to true/false for newsletter_subscribe)\n* We don't need ActiveRecord validations\n* We don't need callbacks on our models -- everything is in the execute method (helper methods are also encouraged)\n* We don't use accepts_nested_attributes_for, even though multiple ActiveRecord models are created\n* This code is completely re-usable in other contexts (need an API?)\n* The inputs to this 'function' are documented by default -- the bare minimum to use it (name and email) are documented, as are 'extras' (newsletter_subscribe)\n\n## Why is it called 'mutations'?\n\nImagine you had a folder in your Rails project:\n\n    app/mutations\n\nAnd inside, you had a library of business operations that you can do against your datastore:\n\n    app/mutations/users/signup.rb\n    app/mutations/users/login.rb\n    app/mutations/users/update_profile.rb\n    app/mutations/users/change_password.rb\n    ...\n    app/mutations/articles/create.rb\n    app/mutations/articles/update.rb\n    app/mutations/articles/publish.rb\n    app/mutations/articles/comment.rb\n    ...\n    app/mutations/ideas/upsert.rb\n    ...\n\nEach of these _mutations_ takes your application from one state to the next.\n\nThat being said, you can create commands for things that don't mutate your database.\n\n## How do I call mutations?\n\nYou have two choices. Given a mutation UserSignup, you can do this:\n\n```ruby\noutcome = UserSignup.run(params)\nif outcome.success?\n  user = outcome.result\nelse\n  render outcome.errors\nend\n```\n\nOr, you can do this:\n\n```ruby\nuser = UserSignup.run!(params) # returns the result of execute, or raises Mutations::ValidationException\n```\n\n## What can I pass to mutations?\n\nMutations only accept hashes as arguments to #run and #run!\n\nThat being said, you can pass multiple hashes to run, and they are merged together. Later hashes take precedence. This give you safety in situations where you want to pass unsafe user inputs and safe server inputs into a single mutation. For instance:\n\n```ruby\n# A user comments on an article\nclass CreateComment \u003c Mutations::Command\n  required do\n    model :user\n    model :article\n    string :comment, max_length: 500\n  end\n\n  def execute; ...; end\nend\n\ndef somewhere\n  outcome = CreateComment.run(params[:comment],\n    user: current_user,\n    article: Article.find(params[:article_id])\n  )\nend\n```\n\nHere, we pass two hashes to CreateComment. Even if the params[:comment] hash has a user or article field, they're overwritten by the second hash. (Also note: even if they weren't, they couldn't be of the correct data type in this particular case.)\n\n## How do I define mutations?\n\n1. Subclass Mutations::Command\n\n    ```ruby\n    class YourMutation \u003c Mutations::Command\n      # ...\n    end\n    ```\n\n2. Define your required inputs and their validations:\n\n    ```ruby\n    required do\n      string :name, max_length: 10\n      symbol :state, in: %i(AL AK AR ... WY)\n      integer :age\n      boolean :is_special, default: true\n      model :account\n    end\n    ```\n\n3. Define your optional inputs and their validations:\n\n    ```ruby\n    optional do\n      array :tags, class: String\n      hash :prefs do\n        boolean :smoking\n        boolean :view\n      end\n    end\n    ```\n\n4. Define your execute method. It can return a value:\n\n    ```ruby\n    def execute\n      record = do_thing(inputs)\n      # ...\n      record\n    end\n    ```\n\nSee a full list of options [here](https://github.com/cypriss/mutations/wiki/Filtering-Input).\n\n## How do I write an execute method?\n\nYour execute method has access to the inputs passed into it:\n\n```ruby\nself.inputs # white-listed hash of all inputs passed to run.  Hash has indifferent access.\n```\n\nIf you define an input called _email_, then you'll have these three methods:\n\n```ruby\nself.email           # Email value passed in\nself.email=(val)     # You can set the email value in execute. Rare, but useful at times.\nself.email_present?  # Was an email value passed in? Useful for optional inputs.\n```\n\nYou can do extra validation inside of execute:\n\n```ruby\nif email =~ /aol.com/\n  add_error(:email, :old_school, \"Wow, you still use AOL?\")\n  return\nend\n```\n\nYou can return a value as the result of the command:\n\n```ruby\ndef execute\n  # ...\n  \"WIN!\"\nend\n\n# Get result:\noutcome = YourMutuation.run(...)\noutcome.result # =\u003e \"WIN!\"\n```\n\n## What about validation errors?\n\nIf things don't pan out, you'll get back an Mutations::ErrorHash object that maps invalid inputs to either symbols or messages. Example:\n\n```ruby\n# Didn't pass required field 'email', and newsletter_subscribe is the wrong format:\noutcome = UserSignup.run(name: \"Bob\", newsletter_subscribe: \"Wat\")\n\nunless outcome.success?\n  outcome.errors.symbolic # =\u003e {email: :required, newsletter_subscribe: :boolean}\n  outcome.errors.message # =\u003e {email: \"Email is required\", newsletter_subscribe: \"Newsletter Subscription isn't a boolean\"}\n  outcome.errors.message_list # =\u003e [\"Email is required\", \"Newsletter Subscription isn't a boolean\"]\nend\n```\n\nYou can add errors in a validate method if the default validations are insufficient. Errors added by validate will prevent the execute method from running.\n\n```ruby\n#...\ndef validate\n  if password != password_confirmation\n    add_error(:password_confirmation, :doesnt_match, \"Your passwords don't match\")\n  end\nend\n# ...\n\n# That error would show up in the errors hash:\noutcome.errors.symbolic # =\u003e {password_confirmation: :doesnt_match}\noutcome.errors.message # =\u003e {password_confirmation: \"Your passwords don't match\"}\n```\n\nAlternatively you can also add these validations in the execute method:\n\n```ruby\n#...\ndef execute\n  if password != password_confirmation\n    add_error(:password_confirmation, :doesnt_match, \"Your passwords don't match\")\n    return\n  end\nend\n# ...\n\n# That error would show up in the errors hash:\noutcome.errors.symbolic # =\u003e {password_confirmation: :doesnt_match}\noutcome.errors.message # =\u003e {password_confirmation: \"Your passwords don't match\"}\n```\n\nIf you want to tie the validation messages into your I18n system, you'll need to [write a custom error message generator](https://github.com/cypriss/mutations/wiki/Custom-Error-Messages).\n\n## FAQs\n\n### Is this better than the 'Rails Way'?\n\nRails comes with an awesome default stack, and a lot of standard practices that folks use are very reasonable (eg, thin controllers, fat models).\n\nThat being said, there's a whole slew of patterns that are available to experienced developers. As your Rails app grows in size and complexity, my experience has been that some of these patterns can help your app immensely.\n\n### How do I share code between mutations?\n\nWrite some modules that you include into multiple mutations.\n\n### Can I subclass my mutations?\n\nYes, but I don't think it's a very good idea. Better to compose.\n\n### Can I use this with Rails forms helpers?\n\nSomewhat. Any form can submit to your server, and mutations will happily accept that input. However, if there are errors, there's no built-in way to bake the errors into the HTML with Rails form tag helpers. Right now this is really designed to support a JSON API.  You'd probably have to write an adapter of some kind.\n","funding_links":[],"categories":["Business logic","Abstraction","Ruby"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcypriss%2Fmutations","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcypriss%2Fmutations","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcypriss%2Fmutations/lists"}