{"id":28301489,"url":"https://github.com/wscourge/nested-generators","last_synced_at":"2025-10-08T18:24:38.621Z","repository":{"id":34953093,"uuid":"193203182","full_name":"wscourge/nested-generators","owner":"wscourge","description":"Opininonated, extendable ServiceObject and QueryObject rails generators with RSpec","archived":false,"fork":false,"pushed_at":"2023-01-19T13:27:51.000Z","size":37,"stargazers_count":2,"open_issues_count":9,"forks_count":0,"subscribers_count":0,"default_branch":"develop","last_synced_at":"2025-09-05T22:56:35.750Z","etag":null,"topics":["rails","ruby-gem","ruby-on-rails","rubygem"],"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/wscourge.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-06-22T07:06:27.000Z","updated_at":"2019-07-05T12:30:55.000Z","dependencies_parsed_at":"2023-02-11T01:30:29.071Z","dependency_job_id":null,"html_url":"https://github.com/wscourge/nested-generators","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/wscourge/nested-generators","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wscourge%2Fnested-generators","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wscourge%2Fnested-generators/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wscourge%2Fnested-generators/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wscourge%2Fnested-generators/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/wscourge","download_url":"https://codeload.github.com/wscourge/nested-generators/tar.gz/refs/heads/develop","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wscourge%2Fnested-generators/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":278556190,"owners_count":26006080,"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","status":"online","status_checked_at":"2025-10-06T02:00:05.630Z","response_time":65,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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-gem","ruby-on-rails","rubygem"],"created_at":"2025-05-23T20:11:32.504Z","updated_at":"2025-10-08T18:24:38.615Z","avatar_url":"https://github.com/wscourge.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# NestedGenerators\n\n[![Gem Version][GV img]][Gem Version]\n[![Build Status][BS img]][Build Status]\n[![Coverage Status][CS img]][Coverage Status]\n\nRuby on Rails generators for `ServiceObject` and `QueryObject` classes and their\ncorresponding unit test files for RSpec framework. Easily extendable.\n\n## Introduction\n\nRuby on Rails enforces unique class names usage inside its `app` directory.\n\nFollowing framework's convention to place different class patterns in their own\nfolders, the gem extends autoloaded `app` directory with other types (like\n[Sidekiq][sidekiq] does with its `app/workers` and\n`rails generate sidekiq:worker` generator):\n\n```\napp\n├── controllers\n│   ├── application_controller.rb\n│   └── concerns\n├── jobs\n│   └── application_job.rb\n├── models\n│   ├── application_record.rb\n│   └── concerns\n├── queries\n└── services\n    └── application_service.rb\n```\n\n## ServiceObject Generator\n\nAs [an old but gold article by CodeClimate][codeclimate article]\nstates in their second point _2. Extract Service Objects_, there are some\ncriteria for when to use `ServiceObject` pattern - read it, its worth it.\n\n### Basic usage\n\nThe gem gives you an ability to generate file structure for your\n`ServiceObject` classes via CLI, with a single empty `public` method `call`.\n\nThe simplest:\n\n```\n$ rails generate service potato_peel\n```\n\nor\n\n```\n$ rails generate service PotatoPeel\n```\n\nresults in following:\n\n```\n├── app\n│   └── services\n│       ├── application_service.rb\n│       └── potato_peel_service.rb\n├── spec\n    └── services\n        └── potato_peel_service_spec.rb\n```\n\nso it gives us three files in total:\n\n**file 1:** _app/services/application_service.rb_\n```\n# frozen_string_literal: true\n\nmodule Services\n  class ApplicationService\n    def self.call(*args)\n      new(*args).call\n    end\n  end\nend\n\n\n```\n\nThe first file contains `ApplicationService`, which simply allows us to call\nthe `ServiceObject` classes without invoking their `.new` method - simply pass\nall your wannabe instance variables to the `.call` method directly:\n\n```\nPotatoPeelService.call(difficulty: 'medium')\n```\n\nresults in the new instance of `PotatoPeelService` with an instance variable\n`@difficulty = 'medium'`.\n\n**file 2:** _app/services/potato_peel_service.rb_\n```\n# frozen_string_literal: true\n\nmodule Services\n  class PotatoPeelService \u003c ApplicationService\n    def initialize\n    end\n\n    def call\n    end\n  end\nend\n\n```\n\nThe second file contains `PotatoPeelService`, which inherits from the\n`ApplicationService` and has two empty methods: `initialize` and `call`.\n\nAs you have probably noticed, `ServiceObjects` are placed in `Services` module.\nI like to keep those that way (hence _opinionated_), but thankfully Rails is\nsmart enough to find their invocation without `Services::` prefix, so you can\nuse both:\n\n```\nServices::PotatoPeelService.call\n# and\nPotatoPeelService.call\n```\n\n**file 3:** _spec/services/potato_peel_service_spec.rb_\n```\n# frozen_string_literal: true\n\nRSpec.describe Services::PotatoPeelService do\n  describe '#call' do\n    pending 'add some examples to (or delete) spec/services/potato_peel_service_spec.rb#call'\n  end\nend\n\n```\n\nThe third file contains almost empty struture to test our brand new\n`PotatoPeelService`, with single `describe` block for the `call` method.\n\n## Generating methods\n\nThe gem allows us to predefine methods for our `ServiceObjects`.\n\n```\nrails generate service potato_peel public:fast protected:deadline private:sharpen_knife\n```\n\nresults in:\n\n**file 1:** _app/services/potato_peel_service.rb_\n```\n# frozen_string_literal: true\n\nmodule Services\n  class PotatoPeelService \u003c ApplicationService\n    def initialize\n    end\n    \n    def call\n    end\n\n    def fast\n    end\n\n    protected\n\n    def deadline\n    end\n\n    private\n\n    def sharpen_knife\n    end\n  end\nend\n```\n\nwhich is the same file structure as before with additional class methods scoped\naccordingly.\n\n**file 2:** _spec/services/potato_peel_service_spec.rb_\n```\n# frozen_string_literal: true\n\nRSpec.describe Services::PotatoPeelService do\n  describe '#call' do\n    pending 'add some examples to (or delete) spec/services/potato_peel_service_spec.rb#call\n  end\n  \n  describe '#fast' do\n    pending 'add some examples to (or delete) spec/services/potato_peel_service_spec.rb#fast\n  end\nend\n```\n\nand the `_spec.rb` file contains an additional `describe` block for the `public`\nmethod passed.\n\n## Submodules\n\nIt is possible to generate service objects with additional namespaces -\ngenerator syntax is the same as with Rails controllers:\n\n```\n$ rails generate service peelers/potato\n```\n\nor even deeper, spearating your modules with backslashes `/`.\n\nThe command call above results in the following:\n\n\n```\n├── app\n│   └── services\n│       ├── application_service.rb\n│       └── peelers\n│           └── potato_service.rb\n└── spec\n     └── services\n          └── peelers\n               └── potato_service_spec.rb\n```\n\n**file 1:** _app/services/peelers/potato_service.rb_\n```\n# frozen_string_literal: true\n\nmodule Services\n  module Peelers\n    class PotatoService \u003c ApplicationService\n      def initialize\n      end\n\n      def call\n      end\n    end\n  end\nend\n\n```\n\n**file 2:** _spec/services/peelers/potato_service_spec.rb_\n```\n# frozen_string_literal: true\n\nRSpec.describe Services::Peelers::PotatoService do\n  describe '#call' do  \n    pending 'add some examples to (or delete) spec/services/peelers/potato_service_spec.rb#call\n  end\nend\n\n```\n\nAnd you can pass them `public`, `protected` and `private` methods too, as\ndescribed in Generating methods section:\n\n```\n$ rails generate service peelers/potato public:fast\n```\n\nresults in:\n\n**file 1:** _app/services/peelers/potato_service.rb_\n```\n# frozen_string_literal: true\n\nmodule Services\n  module Peelers\n    class PotatoService \u003c ApplicationService\n      def initialize\n      end\n\n      def call\n      end\n\n      def fast\n      end\n    end\n  end\nend\n\n```\n\nand corresponding:\n\n**file 2:** _spec/services/peelers/potato_service_spec.rb_\n```\n# frozen_string_literal: true\n\nRSpec.describe Services::Peelers::PotatoService do\n  describe '#call' do\n    pending 'add some examples to (or delete) spec/services/peelers/potato_service_spec.rb#call'\n  end\n\n  describe '#fast' do\n    pending 'add some examples to (or delete) spec/services/peelers/potato_service_spec.rb#fast'\n  end\nend\n\n```\n\n## QueryObject Generator\n\nQuoting the golden article mentioned before:\n\n\u003e For complex SQL queries littering the definition of your ActiveRecord subclass\n(either as scopes or class methods), consider extracting query objects.\n\n\n### Basic usage\n\nThe gem gives you an ability to generate classes structure for your\n`QueryObject` classes via CLI.\n\nThe simplest:\n\n```\n$ rails generate query rotten_potatoes\n```\n\nor\n\n```\n$ rails generate query rotten_potatoes\n```\n\nresults in following:\n\n```\n├── app\n│   └── queries\n│        └── rotten_potatoes_query.rb\n└── spec\n     └── queries\n          └── rotten_potatoes_query_spec.rb\n```\n\nso it gives us two files in total:\n\n**file 1:** _app/queries/rotten_potatoes_query.rb_\n```\n# frozen_string_literal: true\n\nmodule Queries\n  class RottenPotatoesQuery \u003c ApplicationService\n    def initialize(relation)\n      @relation = relation\n    end\n  end\nend\n\n```\n\nThe first file contains `RottenPotatoesQuery` which has single `initialize`\nmethod, with a single `relation` argument assigned to the `@relation` instance\nvariable.\n\nAs you have probably noticed, `QueryObjects` are placed in `Queries` module,\njust like `ServiceObjects` are placed in the `Services` module. I like to keep\nthose that way (hence _opinionated_), but thankfully Rails is smart enough to\nfind their invocation without `Queries::` prefix, so you can use both:\n\n```\nQueries::RottenPotatoesQuery.new\n# and\nRottenPotatoesQuery.new\n```\n\n**file 2:** _spec/services/potato_peel_service_spec.rb_\n```\n# frozen_string_literal: true\n\nRSpec.describe Queries::RottenPotatoesQuery do\n  pending 'add some examples to (or delete) spec/queries/rotten_potatoes_query_spec.rb'\nend\n\n```\n\nThe second file contains an empty struture to test our brand new `RottenPotatoesQuery`.\n\n### Generating methods\n\nThe gem allows us to predefine methods for our `QueryObjects`, same as for `ServiceObjects`.\n\n```\nrails generate query rotten_potatoes public:find protected:empty_bag private:out_of_date\n```\n\nresults in:\n\n**file 1:** _app/queries/rotten_potatoes_query.rb_\n```\n# frozen_string_literal: true\n\nmodule Queries\n  class RottenPotatoesQuery\n    def initialize(relation)\n      @relation = relation\n    end\n\n    def find\n    end\n\n    protected\n\n    def empty_bag\n    end\n\n    private\n\n    def out_of_date\n    end\n  end\nend\n\n```\n\nwhich is the same file structure as before with additional class methods scoped\naccordingly.\n\n**file 2:** _spec/services/potato_peel_service_spec.rb_\n```\n# frozen_string_literal: true\n\nRSpec.describe Queries::RottenPotatoesQuery do\n  describe '#find' do\n    pending 'add some examples to (or delete) spec/queries/rotten_potatoes_query_spec.rb#find'\n  end\nend\n\n```\n\nand the `_spec.rb` file a `describe` block for the `public` method passed.\n\n### Submodules\n\nIt is possible to generate query objects with additional namespaces - generator\nsyntax is the same as with Rails controllers or `ServiceObjects`:\n\n```\n$ rails generate query rotten_vegetables/potatoes\n```\n\nor even deeper, spearating your modules with backslashes `/`.\n\nThe command call above results in the following:\n\n\n```\n├── app\n│   └── queries\n│       └── rotten_vegetables\n│           └── potatoes_query.rb\n└── spec\n     └── queries\n          └── rotten_vegetables\n              └── potatoes_query_spec.rb\n\n```\n\n**file 1:** _app/queries/rotten_vegetables/potatoes_query.rb_\n```\n# frozen_string_literal: true\n\nmodule Queries\n  module RottenVegetables\n    class PotatoesQuery\n      def initialize(relation)\n        @relation = relation\n      end\n    end\n  end\nend\n\n```\n\n**file 2:** _spec/queries/rotten_vegetables/potatoes_query_spec.rb_\n```\n# frozen_string_literal: true\n\nRSpec.describe Queries::RottenVegetables::PotatoesQuery do\n  pending 'add some examples to (or delete) spec/queries/rotten_vegetables/potatoes_query_spec.rb'\nend\n\n```\n\nAnd you can pass them `public`, `protected` and `private` methods too, as\ndescribed in Generating methods section:\n\n```\n$ rails generate query rotten_vegetables/potatoes public:find\n```\n\nresults in:\n\n**file 1:** _app/services/peelers/potato_service.rb_\n```\n# frozen_string_literal: true\n\nmodule Queries\n  module RottenVegetables\n    class PotatoesQuery\n      def initialize(relation)\n        @relation = relation\n      end\n\n      def find\n      end\n    end\n  end\nend\n\n```\n\nand corresponding:\n\n**file 2:** _spec/services/peelers/potato_service_spec.rb_\n```\n# frozen_string_literal: true\n\nRSpec.describe Queries::RottenVegetables::PotatoesQuery do\n  describe '#find' do\n    pending 'add some examples to (or delete) spec/queries/rotten_vegetables/potatoes_query_spec.rb#find'\n  end\nend\n\n```\n\n# Advanced usage - creating your own generators\n\nAs you've probably noticed, `ServiceObject` generator and `QueryObject`\ngenerator are very similar. They both inherit from the\n`NestedGeneratorsBaseGenerator` class, which includes bunch of helpful methods\nto write your own generator, with support for nesting and passing method names\nvia CLI.\n\n## Step by step guide\n\nIn this short guide, we will create our own `ValueObject` generator:\n\n#### Step 1\n\nIn your `lib` directory, create `generators/value/templates` subdirectories:\n\n```\nmkdir -p lib/generators/value/templates\n```\n\n#### Step 2\n\nCreate our `value_generator.rb`, next to `templates` directory:\n\n```\ntouch lib/generators/value/value_generator.rb\n```\n\n#### Step 3\n\nIn the `lib/generators/value/templates` directory, create two templates, one\nfor the `ValueObject` class and second for its tests\n\n```\ntouch lib/generators/templates/class_template.erb\ntouch lib/generators/templates/class_spec_template.erb\n```\n\n#### Step 4\n\nStart with editing `value_generator.rb` file; open it in your text editor and\npaste the following code:\n\n```\n# frozen_string_literal: true\n\nrequire 'generators/nested_generators_base_generator'\n\nclass ValueGenerator \u003c NestedGeneratorsBaseGenerator\n  source_root File.expand_path('templates', __dir__)\n\n  def initialize(*args, \u0026block)\n    super\n    @type = 'value'\n  end\n\n  def create_value_file\n    create_class_file\n    create_class_spec_file\n  end\nend\n\n```\n\n#### Step 5\n\nNext, open the `templates/class_template.erb` and paste the following code:\n\n```\n# frozen_string_literal: true\n\n\u003c%= open_modules_nesting %\u003e\u003c%= open_class %\u003e\n\u003c%= code_indent %\u003einclude Comparable\n\n\u003c%= code_indent %\u003edef initialize\n\u003c%= code_indent %\u003eend\n\n\u003c%- SCOPES.each.with_index do |name, index| -%\u003e\n  \u003c%- if index.zero? -%\u003e\n    \u003c%- if scope?(name) -%\u003e\n      \u003c%- scope_methods(name).each do |method| %\u003e\n\u003c%= code_indent %\u003edef \u003c%= method.gsub(\"#{name}:\", '') %\u003e\n\u003c%= code_indent %\u003eend\n      \u003c%- end -%\u003e\n    \u003c%- end -%\u003e\n  \u003c%- else -%\u003e\n    \u003c%- if scope?(name) -%\u003e\u003c%- %\u003e\n\u003c%- %\u003e\u003c%= code_indent %\u003e\u003c%= name %\u003e\n      \u003c%- scope_methods(name).each do |method| %\u003e\n\u003c%= code_indent %\u003edef \u003c%= method.gsub(\"#{name}:\", '') %\u003e\n\u003c%= code_indent %\u003eend\n      \u003c%- end -%\u003e\n    \u003c%- end -%\u003e\n  \u003c%- end -%\u003e\n\u003c%- end -%\u003e\n\u003c%= end_class %\u003e\u003c%= close_modules_nesting %\u003e\n\n```\n\n#### Step 6\n\nAnd at last, edit the `templates/class_spec_template.erb` with:\n\n```\n# frozen_string_literal: true\n\nRSpec.describe \u003c%= @type.capitalize.pluralize %\u003e::\u003c%= class_name %\u003e\u003c%= @type.capitalize %\u003e do\n  \u003c%- if scope?('public') -%\u003e\n    \u003c%- scope_methods('public').each do |method| -%\u003e\n  describe '#\u003c%= strip_scope(method, 'public') %\u003e' do\n    \u003c%= rspec_empty_message(strip_scope(method, 'public')) %\u003e\n  end\n    \n    \u003c%- end -%\u003e\n  \u003c%- else -%\u003e\n  \u003c%= rspec_empty_message %\u003e\n  \u003c%- end -%\u003e\nend\n\n```\n\n#### Step 7 - Use it!\n\nYour done, congrats. Now, you can use:\n\n```\nrails generate value rating 'public:better_than?' 'public:\u003c=\u003e' public:hash 'public:eql?' public:to_s\n```\n\nresulting in\n\n**file 1:** _app/values/rating_value.rb_\n```\n# frozen_string_literal: true\n\nmodule Values\n  class RatingValue\n    include Comparable\n\n    def initialize\n    end\n\n    def better_than?\n    end\n\n    def \u003c=\u003e\n    end\n\n    def hash\n    end\n\n    def eql?\n    end\n\n    def to_s\n    end\n  end\nend\n```\n\nand\n\n**file 2:** _spec/values/rating_value_spec.rb_\n```\n# frozen_string_literal: true\n\nRSpec.describe Values::RatingValue do\n  describe '#better_than?' do\n    pending 'add some examples to (or delete) spec/values/rating_value_spec.rb#better_than?'\n  end\n\n  describe '#\u003c=\u003e' do\n    pending 'add some examples to (or delete) spec/values/rating_value_spec.rb#\u003c=\u003e'\n  end\n\n  describe '#hash' do\n    pending 'add some examples to (or delete) spec/values/rating_value_spec.rb#hash'\n  end\n\n  describe '#eql?' do\n    pending 'add some examples to (or delete) spec/values/rating_value_spec.rb#eql?'\n  end\n\n  describe '#to_s' do\n    pending 'add some examples to (or delete) spec/values/rating_value_spec.rb#to_s'\n  end\nend\n\n```\n\nAnd the nested version, like `ServiceObject` and `QueryObject` do.\n\n# License\n\n[The MIT License](LICENSE.md)\n\n[sidekiq]: https://github.com/mperham/sidekiq\n[codeclimate article]: https://codeclimate.com/blog/7-ways-to-decompose-fat-activerecord-models/\n\n[Gem Version]: https://rubygems.org/gems/nested-generators\n[Build Status]: https://circleci.com/gh/wscourge/nested-generators/tree/master\n[Coverage Status]: https://coveralls.io/r/wscourge/nested-generators\n\n[GV img]: https://badge.fury.io/rb/nested-generators.svg\n[BS img]: https://circleci.com/gh/wscourge/nested-generators/tree/master.svg?style=shield\n[CS img]: https://coveralls.io/repos/wscourge/nested-generators/badge.svg?branch=master\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwscourge%2Fnested-generators","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwscourge%2Fnested-generators","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwscourge%2Fnested-generators/lists"}