{"id":13879339,"url":"https://github.com/trickstersio/performify","last_synced_at":"2025-10-12T18:30:22.176Z","repository":{"id":56887829,"uuid":"89776508","full_name":"trickstersio/performify","owner":"trickstersio","description":"Service object which makes you better.","archived":false,"fork":false,"pushed_at":"2021-01-05T14:24:23.000Z","size":64,"stargazers_count":15,"open_issues_count":3,"forks_count":2,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-01-29T16:48:36.578Z","etag":null,"topics":["rails","ruby","schema","service-objects"],"latest_commit_sha":null,"homepage":"https://github.com/trickstersio/performify","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/trickstersio.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-04-29T09:44:23.000Z","updated_at":"2023-02-08T16:10:41.000Z","dependencies_parsed_at":"2022-08-21T00:50:52.983Z","dependency_job_id":null,"html_url":"https://github.com/trickstersio/performify","commit_stats":null,"previous_names":["kimrgrey/performify"],"tags_count":17,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/trickstersio%2Fperformify","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/trickstersio%2Fperformify/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/trickstersio%2Fperformify/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/trickstersio%2Fperformify/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/trickstersio","download_url":"https://codeload.github.com/trickstersio/performify/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":236219965,"owners_count":19114261,"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":["rails","ruby","schema","service-objects"],"created_at":"2024-08-06T08:02:17.641Z","updated_at":"2025-10-12T18:30:16.857Z","avatar_url":"https://github.com/trickstersio.png","language":"Ruby","readme":"# Performify\n\nIt's well-known practice that has been proved in many large projects to move server logic into separated service classes. This approach gives a lot of advantages, because when you are able to create object that incapsulates your logic it's much easier to develop, search, control and test. And `performify` helps you to do it in nice and easy way with minimum of pain and maximum of result.\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'performify'\n```\n\nAnd then execute:\n\n```\n$ bundle\n```\n\nOr install it yourself as:\n\n```\n$ gem install performify\n```\n\n## Usage\n\nHow to have a deal with services:\n\n1. Define `ApplicationService`\n2. Create new service inherited from `ApplicationService`\n3. Implement `execute!` method\n4. Use `super` to work with db transaction and automatic success / fail workflow control\n5. Use `success!` and `fail!` to control everything by hands\n6. Use `on_success` / `on_fail` to define callbacks\n7. Use `schema do ... end` if you want to use validations\n\n### ApplicationService\n\nSo, first of all it's better to create `ApplicationService` class that will be used as base for all services in your project. You can put any shared logic (like, authorization, for example) here:\n\n```ruby\nclass ApplicationService \u003c Performify::Base\n  def authorize!(record)\n    # you can put authorization logic here and use it from inherited services\n  end\nend\n```\n\nThis is, for example, how authorization can be implemented for `Pundit`:\n\n```ruby\nclass ApplicationService \u003c Performify::Base\n  def authorize!(record, query = default_query, record_policy = nil)\n    record_policy ||= policy(record)\n    return if record_policy.public_send(query)\n\n    raise Pundit::NotAuthorizedError, query: query, record: record, policy: record_policy\n  end\n\n  def default_query\n    @default_query ||= \"#{self.class.name.demodulize.underscore.to_sym}?\"\n  end\n\n  def policy(record)\n    @policy ||= Pundit.policy!(@current_user, record)\n  end\nend\n```\n\n### Service: database\n\nNow, to define new service just create new class and inherit it from `ApplicationService`:\n\n```ruby\nmodule Users\n  class Destroy \u003c ApplicationService\n    def execute!\n      # current user is already available, so feel free to use it\n      # to get user's context\n\n      authorize! current_user unless force?\n\n      # block passed into super's implementation will be executed\n      # in transaction, so you can do multiple data operations, and final\n      # result of this block will be used to determine result of execution\n\n      super do\n        if current_user.update(destroyed_at: Time.zone.now)\n          current_user.comments.find_each do |c|\n            s = Comments::Destroy.new(current_user, c)\n            s.execute!\n\n            # it's also ok to raise ActiveRecord::Rollback, it will be handled\n            # gracefully as regular execution fail\n\n            raise ActiveRecord::Rollback unless s.success?\n          end\n        end\n      end\n    end\n\n    def force?\n      # additional instance variables can be passed as named args into\n      # initializer and accessed in service flow\n\n      force.present?\n    end\n  end\nend\n```\n\nNow you can create instance of your service and check result of execution:\n\n```ruby\nservice = Users::Destroy.new(current_user, force: true)\nservice.execute!\nservice.success? # or service.fail?\n```\n\n### Service: HTTP API\n\nSometimes your service doesn't work with database, but calls some http endpoint or do some other stuff that doesn't require db transaction. In this case you can control your service flow manually:\n\n```ruby\nclass Stripe::Create \u003c ApplicationService\n  attr_reader :subscription\n\n  def execute!\n    # here you can go to Stripe and create subscription for the user\n    begin\n      @subscription = Stripe::Subscription.create(\n        customer: current_user.customer_id,\n        plan: selected_plan.stripe_name,\n      )\n\n      # everything looks ok, success\n\n      success!\n   rescue Stripe::StripeError =\u003e e\n      # something went wrong, let's notify developers and say that\n      # service execution has been failed\n\n     Airbrake.notify(e)\n     fail!\n   end\n  end\nend\n```\n\n### Callbacks\n\nIf you need to do something on service success / fail it is possible to define appropriate callbacks. Notice, that in case of using `super` callbacks will be executed outside of db transaction, so it's safe to send emails from there, for example.\n\n```ruby\nmodule Passwords\n  class Update \u003c ApplicationService\n    def execute!\n      authorize! current_user\n\n      super do\n        current_user.update(password: password, password_confirmation: password_confirmation)\n      end\n    end\n\n    # you can pass method name as a callback\n\n    on_success :invalidate_sessions\n\n    # or you can pass block instead of method name\n\n    on_success { UserMailer.password_changed(current_user).deliver_later }\n\n    private def invalidate_sessions\n      # you can invalidate existing user's sessions here\n    end\n  end\nend\n```\n\n### Validation\n\nPerformify allows you to validate input arguments using [dry-validation](http://dry-rb.org/gems/dry-validation/) schemas. Validation is performed on creation of service instance. And if validation is not passed it will be impossible to call execution. Result of execution will be automatically switched to failed state.\n\n```ruby\nmodule Users\n  class Create\n    schema do\n      required(:email).filled(:str?)\n    end\n\n    def execute!\n      # it will be impossible to call execution if provided arguments\n      # did not pass validation\n    end\n  end\nend\n\nservice = Users::Create.new(current_user, email: nil)\nservice.execute! # nothing happens here\nservice.success? # will be false because of validation\nservice.errors   # contains hash of errors\n```\n\nSometimes you can have differences between validation errors and execution errors. But usually it's boring to check them separately since you just need to display final result to user. To avoid double check you can use following trick:\n\n```ruby\nmodule Users\n  class Create\n    attr_reader :user\n\n    schema do\n      required(:email).filled(:str?)\n    end\n\n    def execute!\n      user = User.new(email: email)\n      authorize! user\n\n      # Let's assume that user has additional validation of uniqueness on the\n      # level of model, so in controller you need to check separately service's\n      # errors and model's errors, right?\n\n      super { user.save }\n    end\n\n    # So, we define on fail callback where we copy errors from model\n    # to service so now in controller we can check and use only service's errors\n\n    on_fail { errors! user.errors.to_h }\n  end\nend\n\n# in controller\n\nservice = Users::Create.new(current_user, email: nil)\nservice.execute!\n\nif service.success?\n  # respond with ok\nelse\n  # respond with unprocessable entity and service.errors\nend\n```\n\nYou can get filtered inputs after success validation by accessing `inputs`.\n\n```ruby\nmodule Users\n  class UpdateProfile \u003c ApplicationService\n    schema do\n      optional(:first_name).filled(:str?)\n      optional(:last_name).filled(:str?)\n      optional(:login).filled(:str?)\n      optional(:image)\n      optional(:email).filled(format?: /\\A[^ \\n\\r\\s]+@[^ \\n\\r\\s]+\\z/i)\n    end\n\n    def execute!\n      super { current_user.update(inputs) }\n    end\n\n    on_fail { errors!(current_user.errors) }\n  end\nend\n```\n\n## Initialization\n\nPerformify will dynamically define accessors for all arguments passed to service in addition to current_user:\n\n```ruby\nmodule Users\n  class Create\n    def execute!\n      # it will define accessors for all arguments:\n      User.new(email: email, role: role, manager: current_user)\n    end\n  end\nend\n\nservice = Users::Create.new(current_user, email: 'mail@google.com', role: 'employee')\n```\n\nBut if you use `schema` to validate parameters Performify will define accessors only for additional arguments mentioned in schema:\n\n```ruby\nmodule Users\n  class Create\n    schema do\n      required(:email).filled(:str?)\n      optional(:phone).filled(:str?)\n    end\n\n    def execute!\n      # it will define accessors for `email` and `phone`, but won't define `role`\n      User.new(email: email, phone: phone, manager: current_user) # phone is nil\n    end\n  end\nend\n\nservice = Users::Create.new(current_user, email: 'mail@google.com', role: 'manager')\n```\n\n## Development\n\nAfter checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/rspec spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.\n\nTo install this gem onto your local machine, run `bin/rake install`. To release a new version, update the version number in `version.rb`, and then run `bin/rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/kimrgrey/performify.\n\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).\n","funding_links":[],"categories":["Ruby"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftrickstersio%2Fperformify","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftrickstersio%2Fperformify","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftrickstersio%2Fperformify/lists"}