{"id":18336336,"url":"https://github.com/launchpadlab/sweet_actions","last_synced_at":"2025-06-24T16:38:35.132Z","repository":{"id":59156974,"uuid":"101695455","full_name":"LaunchPadLab/sweet_actions","owner":"LaunchPadLab","description":"An object oriented approach to controller actions","archived":false,"fork":false,"pushed_at":"2018-06-13T04:31:25.000Z","size":61,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-06-20T04:39:14.118Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"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/LaunchPadLab.png","metadata":{"files":{"readme":"README.md","changelog":null,"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-08-28T22:59:15.000Z","updated_at":"2018-05-21T03:14:38.000Z","dependencies_parsed_at":"2022-09-13T20:11:30.853Z","dependency_job_id":null,"html_url":"https://github.com/LaunchPadLab/sweet_actions","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/LaunchPadLab/sweet_actions","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LaunchPadLab%2Fsweet_actions","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LaunchPadLab%2Fsweet_actions/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LaunchPadLab%2Fsweet_actions/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LaunchPadLab%2Fsweet_actions/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/LaunchPadLab","download_url":"https://codeload.github.com/LaunchPadLab/sweet_actions/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LaunchPadLab%2Fsweet_actions/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":261715556,"owners_count":23198771,"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-11-05T20:07:34.536Z","updated_at":"2025-06-24T16:38:35.109Z","avatar_url":"https://github.com/LaunchPadLab.png","language":"Ruby","readme":"# Sweet Actions\n\n## Introduction\nController actions (`events#create`) tend to have more in common with their cousins (`articles#create`) than their siblings (`events#show`). Because of this, we think actions should be classes instead of methods. This makes it possible for actions to take advantage of common Object Oriented principles like Inheritance and Composition.\n\nThe end result of this approach is that resource-specific controllers and actions often don't even need to exist - their logic is abstracted to parent actions.\n\nWork smart not hard right?\n\nLet's take a look at how that's possible.\n\n## Installation\n\n### 1. Install Gem\n\nGemfile:\n\n```ruby\ngem 'sweet_actions'\ngem 'active_model_serializers'\ngem 'decanter'\n```\n\nTerminal:\n\n```\nbundle\nbundle exec rails g sweet_actions:install\n```\n\nThis command generates a folder at `app/actions` with the following structure:\n\n```\n- base_action.rb\n- collect_action.rb\n- create_action.rb\n- destroy_action.rb\n- show_action.rb\n- update_action.rb\n```\n\n### 2. Generate Resource\n\n```\nrails g model Event title:string start_date:date\nbundle exec rake db:migrate\nrails g decanter Event title:string start_date:date\nrails g serializer Event title:string start_date:date\nrails g actions Events\n```\n\nThis last command (`rails g actions events`) generates a folder at `app/actions/events` with the following structure:\n\n```\n- events/\n    - collect.rb\n    - create.rb\n    - destroy.rb\n    - show.rb\n    - update.rb\n```\n\n### 3. Add Routes\n\n```ruby\nRails.application.routes.draw do\n  scope :api do\n    scope :v1 do\n      create_sweet_actions(:events)\n    end\n  end\nend\n```\n\n### 4. Profit\n\n```\nrails s\n```\n\nUsing Postman, submit the following request:\n\nPOST to localhost:3000/api/v1/events\n\n```json\n{\n  \"event\": {\n    \"title\": \"My sweet event\",\n    \"start_date\": \"01/18/2018\"\n  }\n}\n```\n\nYou should get a response like so:\n\n```json\n{\n  \"type\": \"event\",\n  \"attributes\": {\n    \"id\": 1,\n    \"title\": \"My sweet event\",\n    \"start_date\": \"2018-01-18\"\n  }\n}\n```\n\n## Default REST Actions\n\nFor a given resource, we provide five RESTful actions:\n\n```\nCollect: GET '/events'\nCreate: POST '/events'\nShow: GET '/events/:id'\nUpdate: PUT '/events/:id'\nDestroy: DELETE '/events/:id'\n```\n\nMany of these actions have shared behavior, which we abstract for you:\n- Authorization of resource (cancancan for example)\n- Create and Update need to be able to properly respond with error information when save does not succeed\n- Create and Update rely on decanted params\n- Serialization of resource\n\n## Creating One-Off Actions\n\nFor actions that are not RESTful (i.e. not one of the five listed above), you can still use `sweet_actions`. For example, let's say you want to create the action `events#export`.\n\n1. Create a new file at app/actions/events/export.rb:\n2. Implement `action` method that responds with a response hash\n3. Create the route\n\n```ruby\n# app/actions/events/export.rb:\nmodule Events\n  class Export \u003c SweetActions::JSON::BaseAction\n    def action\n      {\n        success: true\n      }\n    end\n  end\nend\n```\n\n```ruby\n# config/routes.rb\nRails.application.routes.draw do\n  scope :api\n    scope :v1\n      get '/events/export' =\u003e 'sweet_actions#export', resource_class: 'Event'\n    end\n  end\nend\n```\n\nUsing a tool like Postman, submit the following request:\n\n```\nGET to localhost:3000/api/v1/events/export\n```\n\nYou should get a response like so:\n\n```json\n{\n  success: true\n}\n```\n\n## The Idea Explained in Detail\n\nIn a RESTful context, controller actions tend to have more in common with the same actions belonging to other resources than other actions belonging to the same resource. For example, let's say we have two resources in our app: Events and Articles.\n\nWhich do you think has more in common in terms of programming logic?\n\n1. events#create \u003c=\u003e events#index\n2. events#create \u003c=\u003e articles#create\n\nWe would argue that #2 has more in common than #1. Both events#create and articles#create need to do the following:\n\n1. Authorize the transaction\n2. Validate the data\n3. Persist the new record\n4. Respond with new record (if successful) or error information (if unsuccessful) in the JSON\n\nRails pushes us to organize our actions as methods inside a resource based controller like below. With this approach we cannot take advantage of basic Object Oriented programming concepts like Inheritance and Modules as it relates to specific actions.\n\n```ruby\nclass EventsController \u003c ApplicationController\n  # more similar to articles#create than events#index\n  def create\n    event = Event.new(event_params)\n    raise NotAuthorized unless can?(:create, event)\n\n    if event.save\n      UserMailer.new_event_confirmation(event).deliver_later\n      serialize(event)\n    else\n      serialize_errors(event)\n    end\n  end\n\n  # more similar to articles#index than events#create\n  def index\n    events = Event.where(date: \u003e= Date.today)\n    serialize(events)\n  end\nend\n\nclass ArticlesController \u003c ApplicationController\n  # more similar to events#create than articles#index\n  def create\n    article = Article.new(article_params)\n    raise NotAuthorized unless can?(:create, article)\n\n    if article.save\n      serialize(article)\n    else\n      serialize_errors(article)\n    end\n  end\n\n  # more similar to events#index than articles#create\n  def index\n    articles = Article.where(published: true)\n    serialize(articles)\n  end\nend\n```\n\nInstead, we propose a strategy where the actions themselves are classes. This would allow us have multiple layers of abstraction like so:\n\n1. **Generic Logic** (logic that applies to all apps that use SweetActions):\n- `class SweetActions::JSON::CreateAction`\n\n2. **Application Logic**: logic that applies to all create actions in your app:\n- `class CreateAction \u003c SweetActions::JSON::CreateAction`\n\n3. **Resource Logic**: logic that applies to a specific resource (e.g. Events) in your app\n- `class Events::Create \u003c CreateAction`\n\nWith this approach, we often won't even need to implement resource specific actions. This is because by default our `Events::Create` action will inherit all the functionality it needs. Only when there are deviances from the norm do we implement resource specific classes and in those cases, we need only override the methods that correspond with the deviance.\n\nFor example, let's say we want to send an email when an event is created. It's as easy as overriding the `after_save` hook:\n\n```ruby\n# resource logic for create (app/actions/events/create.rb)\nmodule Events\n  class Create \u003c CreateAction\n    def after_save\n      UserMailer.new_event_confirmation(resource).deliver_later\n    end\n  end\nend\n```\n\nIf we wanted to override all action behavior, we could just implement the `action` method itself:\n\n```ruby\nmodule Events\n  class Create \u003c CreateAction\n    def action\n      event = Event.new(resource_params)\n      event.save ? success(event) : failure\n    end\n\n    def success(event)\n      UserMailer.new_event_confirmation(resource).deliver_later\n      { success: true, data: { event: event } }\n    end\n  end\nend\n```\n\nUnder the hood, this is made possible by a structure that looks like the following:\n\n```ruby\n# generic logic for create (sweet_actions gem)\nmodule SweetActions\n  module JSON\n    class CreateAction \u003c BaseAction\n      def action\n        @resource = set_resource\n        authorize\n        validate_and_save ? success : failure\n      end\n\n      # ...\n    end\n  end\nend\n```\n\n```ruby\n# app logic for create (app/actions/create_action.rb)\nclass CreateAction \u003c SweetActions::JSON::CreateAction\n  def set_resource\n    resource_class.new(resource_params)\n  end\n\n  def authorized?\n    can?(:create, resource)\n  end\nend\n```\n\n```ruby\n# resource logic for create (app/actions/events/create.rb)\nmodule Events\n  class Create \u003c CreateAction\n    def after_save\n      UserMailer.new_event_confirmation(resource).deliver_later\n    end\n  end\nend\n```\n\nAs you can see, we can abstract most of the `create` logic to be shared across resources, which means you **only need to write the code that is unique about this create action vs. other create actions**.\n\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flaunchpadlab%2Fsweet_actions","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flaunchpadlab%2Fsweet_actions","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flaunchpadlab%2Fsweet_actions/lists"}