├── .github └── workflows │ └── gempush.yml ├── .gitignore ├── .rspec ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── command_service_object.gemspec ├── lib ├── command_service_object.rb ├── command_service_object │ ├── configuration.rb │ ├── failure.rb │ ├── helpers │ │ ├── check_helper.rb │ │ ├── controller_helper.rb │ │ ├── failure_helper.rb │ │ └── model_helper.rb │ ├── hooks.rb │ ├── railtie.rb │ └── version.rb └── generators │ ├── .DS_Store │ └── service │ ├── USAGE │ ├── command │ ├── USAGE │ ├── command_generator.rb │ └── templates │ │ ├── command.rb.erb │ │ └── command_spec.rb.erb │ ├── entity │ ├── entity_generator.rb │ └── templates │ │ └── entity.rb.erb │ ├── external │ ├── external_generator.rb │ └── templates │ │ ├── external.rb.erb │ │ ├── external_spec.rb.erb │ │ └── external_test.rb.erb │ ├── helper.rb │ ├── install │ ├── install_generator.rb │ └── templates │ │ ├── initializer.rb │ │ └── services │ │ ├── application_service.rb │ │ ├── case_base.rb │ │ ├── command_base.rb │ │ ├── issuer.rb │ │ ├── listener_base.rb │ │ ├── query_base.rb │ │ ├── service_result.rb │ │ ├── util │ │ └── json.rb │ │ └── value_object_base.rb │ ├── listener │ ├── listener_generator.rb │ └── templates │ │ └── listener.rb.erb │ ├── micro │ ├── micro_generator.rb │ └── templates │ │ └── micro.rb.erb │ ├── query │ ├── USAGE │ ├── query_generator.rb │ └── templates │ │ └── query.rb.erb │ ├── service_generator.rb │ ├── setup │ ├── setup_generator.rb │ └── templates │ │ └── doc.md.erb │ ├── usecase │ ├── USAGE │ ├── templates │ │ ├── usecase.rb.erb │ │ └── usecase_spec.rb.erb │ └── usecase_generator.rb │ └── value_object │ ├── templates │ └── value_object.rb.erb │ └── value_object_generator.rb └── test ├── command_service_object_test.rb ├── generators ├── install_generator_test.rb └── usecase_generator_test.rb ├── support └── generator_test_helpers.rb └── test_helper.rb /.github/workflows/gempush.yml: -------------------------------------------------------------------------------- 1 | name: Ruby Gem 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | push: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | name: Build + Publish 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@master 18 | - name: Set up Ruby 2.6 19 | uses: actions/setup-ruby@v1 20 | with: 21 | version: 2.6.x 22 | 23 | - name: Publish to GPR 24 | run: | 25 | mkdir -p $HOME/.gem 26 | touch $HOME/.gem/credentials 27 | chmod 0600 $HOME/.gem/credentials 28 | printf -- "---\n:github: Bearer ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials 29 | gem build *.gemspec 30 | gem push --KEY github --host https://rubygems.pkg.github.com/${OWNER} *.gem 31 | env: 32 | GEM_HOST_API_KEY: ${{secrets.GPR_AUTH_TOKEN}} 33 | OWNER: username 34 | 35 | - name: Publish to RubyGems 36 | run: | 37 | mkdir -p $HOME/.gem 38 | touch $HOME/.gem/credentials 39 | chmod 0600 $HOME/.gem/credentials 40 | printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials 41 | gem build *.gemspec 42 | gem push *.gem 43 | env: 44 | GEM_HOST_API_KEY: ${{secrets.RUBYGEMS_AUTH_TOKEN}} 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | *.gem 10 | .byebug_history 11 | 12 | # rspec failure tracking 13 | .rspec_status 14 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | sudo: false 3 | language: ruby 4 | cache: bundler 5 | rvm: 6 | - 2.6.0 7 | before_install: gem install bundler -v 2.0.1 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [v0.4.0](https://github.com/adham90/command_service_object/tree/v0.4.0) (2019-04-12) 4 | **Closed issues:** 5 | 6 | - Generating Specs [\#1](https://github.com/adham90/command_service_object/issues/1) 7 | 8 | 9 | 10 | \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at adham.eldeeb90@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | ## Pull Request Process 9 | 10 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a 11 | build. 12 | 2. Update the README.md with details of changes to the interface, this includes new environment 13 | variables, exposed ports, useful file locations and container parameters. 14 | 3. Increase the version numbers in any examples files and the README.md to the new version that this 15 | Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). 16 | 4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you 17 | do not have permission to do that, you may request the second reviewer to merge it for you. 18 | 19 | ## Code of Conduct 20 | 21 | ### Our Pledge 22 | 23 | In the interest of fostering an open and welcoming environment, we as 24 | contributors and maintainers pledge to making participation in our project and 25 | our community a harassment-free experience for everyone, regardless of age, body 26 | size, disability, ethnicity, gender identity and expression, level of experience, 27 | nationality, personal appearance, race, religion, or sexual identity and 28 | orientation. 29 | 30 | ### Our Standards 31 | 32 | Examples of behavior that contributes to creating a positive environment 33 | include: 34 | 35 | * Using welcoming and inclusive language 36 | * Being respectful of differing viewpoints and experiences 37 | * Gracefully accepting constructive criticism 38 | * Focusing on what is best for the community 39 | * Showing empathy towards other community members 40 | 41 | Examples of unacceptable behavior by participants include: 42 | 43 | * The use of sexualized language or imagery and unwelcome sexual attention or 44 | advances 45 | * Trolling, insulting/derogatory comments, and personal or political attacks 46 | * Public or private harassment 47 | * Publishing others' private information, such as a physical or electronic 48 | address, without explicit permission 49 | * Other conduct which could reasonably be considered inappropriate in a 50 | professional setting 51 | 52 | ### Our Responsibilities 53 | 54 | Project maintainers are responsible for clarifying the standards of acceptable 55 | behavior and are expected to take appropriate and fair corrective action in 56 | response to any instances of unacceptable behavior. 57 | 58 | Project maintainers have the right and responsibility to remove, edit, or 59 | reject comments, commits, code, wiki edits, issues, and other contributions 60 | that are not aligned to this Code of Conduct, or to ban temporarily or 61 | permanently any contributor for other behaviors that they deem inappropriate, 62 | threatening, offensive, or harmful. 63 | 64 | ### Scope 65 | 66 | This Code of Conduct applies both within project spaces and in public spaces 67 | when an individual is representing the project or its community. Examples of 68 | representing a project or community include using an official project e-mail 69 | address, posting via an official social media account, or acting as an appointed 70 | representative at an online or offline event. Representation of a project may be 71 | further defined and clarified by project maintainers. 72 | 73 | ### Enforcement 74 | 75 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 76 | reported by contacting the project team at [INSERT EMAIL ADDRESS]. All 77 | complaints will be reviewed and investigated and will result in a response that 78 | is deemed necessary and appropriate to the circumstances. The project team is 79 | obligated to maintain confidentiality with regard to the reporter of an incident. 80 | Further details of specific enforcement policies may be posted separately. 81 | 82 | Project maintainers who do not follow or enforce the Code of Conduct in good 83 | faith may face temporary or permanent repercussions as determined by other 84 | members of the project's leadership. 85 | 86 | ### Attribution 87 | 88 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 89 | available at [http://contributor-covenant.org/version/1/4][version] 90 | 91 | [homepage]: http://contributor-covenant.org 92 | [version]: http://contributor-covenant.org/version/1/4/ 93 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in command_service_object.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | command_service_object (1.4.0) 5 | hutch (~> 1.0) 6 | virtus (~> 1.0, >= 1.0.5) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | actioncable (5.2.4.4) 12 | actionpack (= 5.2.4.4) 13 | nio4r (~> 2.0) 14 | websocket-driver (>= 0.6.1) 15 | actionmailer (5.2.4.4) 16 | actionpack (= 5.2.4.4) 17 | actionview (= 5.2.4.4) 18 | activejob (= 5.2.4.4) 19 | mail (~> 2.5, >= 2.5.4) 20 | rails-dom-testing (~> 2.0) 21 | actionpack (5.2.4.4) 22 | actionview (= 5.2.4.4) 23 | activesupport (= 5.2.4.4) 24 | rack (~> 2.0, >= 2.0.8) 25 | rack-test (>= 0.6.3) 26 | rails-dom-testing (~> 2.0) 27 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 28 | actionview (5.2.4.4) 29 | activesupport (= 5.2.4.4) 30 | builder (~> 3.1) 31 | erubi (~> 1.4) 32 | rails-dom-testing (~> 2.0) 33 | rails-html-sanitizer (~> 1.0, >= 1.0.3) 34 | activejob (5.2.4.4) 35 | activesupport (= 5.2.4.4) 36 | globalid (>= 0.3.6) 37 | activemodel (5.2.4.4) 38 | activesupport (= 5.2.4.4) 39 | activerecord (5.2.4.4) 40 | activemodel (= 5.2.4.4) 41 | activesupport (= 5.2.4.4) 42 | arel (>= 9.0) 43 | activestorage (5.2.4.4) 44 | actionpack (= 5.2.4.4) 45 | activerecord (= 5.2.4.4) 46 | marcel (~> 0.3.1) 47 | activesupport (5.2.4.4) 48 | concurrent-ruby (~> 1.0, >= 1.0.2) 49 | i18n (>= 0.7, < 2) 50 | minitest (~> 5.1) 51 | tzinfo (~> 1.1) 52 | amq-protocol (2.3.2) 53 | arel (9.0.0) 54 | axiom-types (0.1.1) 55 | descendants_tracker (~> 0.0.4) 56 | ice_nine (~> 0.11.0) 57 | thread_safe (~> 0.3, >= 0.3.1) 58 | builder (3.2.4) 59 | bunny (2.15.0) 60 | amq-protocol (~> 2.3, >= 2.3.1) 61 | byebug (9.0.6) 62 | carrot-top (0.0.7) 63 | json 64 | coercible (1.0.0) 65 | descendants_tracker (~> 0.0.1) 66 | concurrent-ruby (1.1.7) 67 | crass (1.0.6) 68 | descendants_tracker (0.0.4) 69 | thread_safe (~> 0.3, >= 0.3.1) 70 | equalizer (0.0.11) 71 | erubi (1.9.0) 72 | globalid (0.4.2) 73 | activesupport (>= 4.2.0) 74 | hutch (1.0.0) 75 | activesupport (>= 4.2, < 7) 76 | bunny (>= 2.15, < 2.16) 77 | carrot-top (~> 0.0.7) 78 | multi_json (~> 1.14) 79 | i18n (1.8.5) 80 | concurrent-ruby (~> 1.0) 81 | ice_nine (0.11.2) 82 | json (2.3.1) 83 | loofah (2.7.0) 84 | crass (~> 1.0.2) 85 | nokogiri (>= 1.5.9) 86 | mail (2.7.1) 87 | mini_mime (>= 0.1.1) 88 | marcel (0.3.3) 89 | mimemagic (~> 0.3.2) 90 | method_source (1.0.0) 91 | mimemagic (0.3.5) 92 | mini_mime (1.0.2) 93 | mini_portile2 (2.4.0) 94 | minitest (5.14.2) 95 | multi_json (1.15.0) 96 | nio4r (2.5.4) 97 | nokogiri (1.10.10) 98 | mini_portile2 (~> 2.4.0) 99 | rack (2.2.3) 100 | rack-test (1.1.0) 101 | rack (>= 1.0, < 3) 102 | rails (5.2.4.4) 103 | actioncable (= 5.2.4.4) 104 | actionmailer (= 5.2.4.4) 105 | actionpack (= 5.2.4.4) 106 | actionview (= 5.2.4.4) 107 | activejob (= 5.2.4.4) 108 | activemodel (= 5.2.4.4) 109 | activerecord (= 5.2.4.4) 110 | activestorage (= 5.2.4.4) 111 | activesupport (= 5.2.4.4) 112 | bundler (>= 1.3.0) 113 | railties (= 5.2.4.4) 114 | sprockets-rails (>= 2.0.0) 115 | rails-dom-testing (2.0.3) 116 | activesupport (>= 4.2.0) 117 | nokogiri (>= 1.6) 118 | rails-html-sanitizer (1.3.0) 119 | loofah (~> 2.3) 120 | railties (5.2.4.4) 121 | actionpack (= 5.2.4.4) 122 | activesupport (= 5.2.4.4) 123 | method_source 124 | rake (>= 0.8.7) 125 | thor (>= 0.19.0, < 2.0) 126 | rake (13.0.1) 127 | sprockets (4.0.2) 128 | concurrent-ruby (~> 1.0) 129 | rack (> 1, < 3) 130 | sprockets-rails (3.2.2) 131 | actionpack (>= 4.0) 132 | activesupport (>= 4.0) 133 | sprockets (>= 3.0.0) 134 | thor (0.20.3) 135 | thread_safe (0.3.6) 136 | tzinfo (1.2.7) 137 | thread_safe (~> 0.1) 138 | virtus (1.0.5) 139 | axiom-types (~> 0.1) 140 | coercible (~> 1.0) 141 | descendants_tracker (~> 0.0, >= 0.0.3) 142 | equalizer (~> 0.0, >= 0.0.9) 143 | websocket-driver (0.7.3) 144 | websocket-extensions (>= 0.1.0) 145 | websocket-extensions (0.1.5) 146 | 147 | PLATFORMS 148 | ruby 149 | 150 | DEPENDENCIES 151 | bundler (~> 2.0) 152 | byebug (~> 9.0.6) 153 | command_service_object! 154 | minitest (~> 5.11, >= 5.11.3) 155 | rails (~> 5.0) 156 | rake (~> 13.0) 157 | thor (~> 0.20.3) 158 | 159 | BUNDLED WITH 160 | 2.1.4 161 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Adham EL-Deeb 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CommandServiceObject 2 | 3 | Rails Generator for command service object. 4 | 5 | ## Theory 6 | 7 | [Command Design Pattern](https://en.wikipedia.org/wiki/Command_pattern) consists of `Command Object` and `Service Object` (Executor), Command object is responsible for containing `Client` requests and run input validations on it to ensure that the request is valid and set default values, then `Service Object` applies the business logic on that command. 8 | 9 | ### Implementation 10 | 11 | Service consists of several objects { `Command Object` `Usecase Object` And `Error Object` (business logic error) }. 12 | 13 | - **[Command](https://en.wikipedia.org/wiki/Command_pattern):** the object that responsible for containing `Client` requests and run input validations it's implemented using [Virtus](https://github.com/solnic/virtus) gem and can use `activerecord` for validations and it's existed under `commands` dir. 14 | - **Usecase:** this object responsible for executing the business logic, Every `usecase` should execute one command type only so that command name should be the same as usecase object name, usecase object existed under 'usecases` dir. 15 | - **Micros:** Small reusable logic under the same service. 16 | - **Externals:** Simple ruby module works as a service interface whenever you wanna call any external service or even service that lives under the same project you should use it. 17 | - **Queries:** This dir is the only entry point for you to get any data form a service. 18 | - **Listeners:** An event listener that waits for an event outside the service to occur. 19 | - **Entities:** Many objects are not fundamentally defined by their attributes, but rather by a thread of continuity and identity. 20 | 21 | ### Result Object 22 | 23 | In case of successful or failure `ApplicationService` the responsible object for all services will return `service_result` object this object contain `value!` method containing successful call result, and `errors` method containing failure `errors` objects. 24 | 25 | ### Helpers: 26 | 27 | - Fail: You can use `fail!` helper to raise business logic failures, ex: `fail!('user should not have any active cards')`. 28 | - Check: To do business logic validations you can use `check!` helper ex: `check!('user should not have any active cards') { user.active_cards.empty? }`, if the given block returns false then it will raise fail! with the given message. 29 | 30 | > You can check if the result successful or not by using `ok?` method. 31 | 32 | ## Installation 33 | 34 | Add this line to your application's Gemfile: 35 | 36 | ```ruby 37 | gem 'command_service_object' 38 | ``` 39 | 40 | And then execute: 41 | 42 | `bundle` 43 | 44 | Or install it yourself as: 45 | 46 | `gem install command_service_object` 47 | 48 | Next, you need to run the generator: 49 | 50 | `rails generate service:install` 51 | 52 | ## Usage 53 | 54 | $ rails g service [service_name] [usecases usecases] 55 | 56 | ### Generate Service ex 57 | 58 | $ rails g service auth login 59 | output 60 | 61 | ```bash 62 | app/services/ 63 | ├── application_service.rb 64 | ├── auth_service 65 | │ ├ external/ 66 | │   ├── commands 67 | │   │   └── login.rb 68 | │   └── usecases 69 | │   ├── login.rb 70 | │   └── micros 71 | ├── case_base.rb 72 | └── service_result.rb 73 | ``` 74 | 75 | ### Generate micros ex 76 | 77 | $ rails g service:micro auth generate_jwt_token_for 78 | 79 | ```bash 80 | app/services/ 81 | ├── auth_service 82 | │   └── usecases 83 | │   └── micros 84 | │   └── generate_jwt_token_for.rb 85 | ``` 86 | 87 | ```ruby 88 | # app/services/auth_service/usecases/micros/generate_jwt_token_for.rb 89 | 90 | module PaymentService::Usecases::Micros 91 | class GenerateJwtTokenFor < CaseBase 92 | def call 93 | # 94 | end 95 | end 96 | end 97 | ``` 98 | 99 | then you can edit command params 100 | > you can read [Virtus gem docs](https://github.com/solnic/virtus) for more info. 101 | 102 | ```ruby 103 | # app/services/auth_service/commands/login.rb 104 | # frozen_string_literal: true 105 | 106 | module AuthService::Commands 107 | class Login < CommandBase 108 | # You can read Virtus gem doc for more info. 109 | # https://github.com/solnic/virtus 110 | 111 | # Attributes 112 | # attribute :REPLACE_ME, String 113 | 114 | # Validations 115 | # validates :REPLACE_ME, presence: true 116 | end 117 | end 118 | ``` 119 | 120 | and then add your business logic 121 | 122 | ```ruby 123 | # app/services/auth_service/usecases/login.rb 124 | # frozen_string_literal: true 125 | 126 | module AuthService::Usecases 127 | class Login < CaseBase 128 | include CommandServiceObject::Hooks 129 | micros :generate_jwt_token_for 130 | # 131 | # Your business logic goes here, keep [call] method clean by using private 132 | # methods for Business logic. 133 | # 134 | def call 135 | token = generate_jwt_token_for(cmd.user) 136 | replace_me 137 | 138 | output 139 | end 140 | 141 | def output 142 | # return entity object 143 | end 144 | 145 | # This method will run if call method raise error 146 | def rollback 147 | # rollback logic 148 | end 149 | 150 | def allowed? 151 | # policies loginc for issuer 152 | # ex: 153 | # 154 | # return false if issuer.role != :admin 155 | 156 | true 157 | end 158 | 159 | private 160 | 161 | def replace_me 162 | # [business logic] 163 | end 164 | end 165 | end 166 | ``` 167 | 168 | ### External APIs or Services 169 | 170 | You can wrap external apis or services under `external/` dir 171 | 172 | #### ex 173 | 174 | ```ruby 175 | module External 176 | module StripeService 177 | extend self 178 | 179 | def charge(customer:, amount:, currency:, description: nil) 180 | Stripe::Charge.create( 181 | customer: customer.id, 182 | amount: (round_up(amount, currency) * 100).to_i, 183 | description: description || customer.email, 184 | currency: currency 185 | ) 186 | end 187 | end 188 | end 189 | ``` 190 | 191 | usage from controller 192 | 193 | ```ruby 194 | class AuthenticationController < ApplicationController 195 | default_service :auth_service 196 | 197 | def Login 198 | cmd = command.new(params) # AuthService::Commands::Login.new 199 | result = execute(cmd) 200 | 201 | if result.ok? 202 | render json: result.value!.as_json, status: 201 203 | else 204 | render json: { message: result.error }, status: 422 205 | end 206 | end 207 | end 208 | ``` 209 | 210 | ## Development 211 | 212 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 213 | 214 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec 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). 215 | 216 | ## Contributing 217 | 218 | Bug reports and pull requests are welcome on GitHub at https://github.com/adham90/command_service_object. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 219 | 220 | ## License 221 | 222 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 223 | 224 | ## Code of Conduct 225 | 226 | Everyone interacting in the CommandServiceObject project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/adham90/command_service_object/blob/master/CODE_OF_CONDUCT.md). 227 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rake/testtask' 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << 'test' 6 | t.libs << 'lib' 7 | t.test_files = FileList['test/**/*_test.rb'] 8 | end 9 | 10 | task default: :test 11 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | require 'command_service_object' 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require 'irb' 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /command_service_object.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('lib', __dir__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require 'command_service_object/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'command_service_object' 7 | spec.version = CommandServiceObject::VERSION 8 | spec.authors = ['Adham EL-Deeb', 'Mohamed Diaa'] 9 | spec.email = ['adham.eldeeb90@gmail.com', 'mohamed.diaa27@gmail.com'] 10 | 11 | spec.summary = 'Rails Generator for command service object.' 12 | spec.description = 'command_service_object gem helps you to generate'\ 13 | ' service and command objects using rails generator.' 14 | spec.homepage = 'https://github.com/adham90/command_service_object' 15 | spec.license = 'MIT' 16 | 17 | spec.files = Dir.chdir(File.expand_path(__dir__)) do 18 | `git ls-files -z`.split("\x0").reject do |f| 19 | f.match(%r{^(test|spec|features)/}) 20 | end 21 | end 22 | 23 | spec.metadata = { 24 | 'homepage_uri' => 'https://github.com/adham90/command_service_object', 25 | 'changelog_uri' => 'https://github.com/adham90/command_service_object/blob/master/CHANGELOG.md', 26 | 'source_code_uri' => 'https://github.com/adham90/command_service_object', 27 | 'bug_tracker_uri' => 'https://github.com/adham90/command_service_object/issues' 28 | } 29 | 30 | spec.bindir = 'exe' 31 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 32 | spec.require_paths = ['lib'] 33 | spec.post_install_message = 'Yaaay!! CommandServiceObject is ready to rock your app!!' 34 | 35 | spec.add_development_dependency 'bundler', '~> 2.0' 36 | spec.add_development_dependency 'byebug', '~> 9.0.6' 37 | spec.add_development_dependency 'minitest', '~> 5.11', '>= 5.11.3' 38 | spec.add_development_dependency 'rails', '~> 5.0' 39 | spec.add_development_dependency 'rake', '~> 13.0' 40 | spec.add_development_dependency 'thor', '~> 0.20.3' 41 | 42 | spec.add_dependency 'virtus', '~> 1.0', '>= 1.0.5' 43 | spec.add_dependency 'hutch', '~> 1.0' 44 | end 45 | -------------------------------------------------------------------------------- /lib/command_service_object.rb: -------------------------------------------------------------------------------- 1 | require 'command_service_object/version' 2 | require 'command_service_object/configuration' 3 | require 'command_service_object/failure' 4 | require 'command_service_object/helpers/model_helper' 5 | require 'command_service_object/helpers/controller_helper' 6 | require 'command_service_object/helpers/failure_helper' 7 | require 'command_service_object/helpers/check_helper' 8 | require 'command_service_object/hooks' 9 | require 'virtus' 10 | 11 | if defined?(Rails) && Rails::VERSION::STRING >= '3.0' 12 | require 'command_service_object/railtie' 13 | end 14 | 15 | module CommandServiceObject 16 | class << self 17 | attr_accessor :configuration 18 | end 19 | 20 | def self.configuration 21 | @configuration ||= Configuration.new 22 | end 23 | 24 | def self.reset 25 | @configuration = Configuration.new 26 | end 27 | 28 | def self.configure 29 | yield(configuration) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/command_service_object/configuration.rb: -------------------------------------------------------------------------------- 1 | module CommandServiceObject 2 | class Configuration 3 | attr_accessor :append_controller_helper, :command_method_name 4 | attr_accessor :skip_command, :skip_test, :skip_error 5 | 6 | def initialize 7 | @append_controller_helper = true 8 | @command_method_name = :command 9 | @skip_command = false 10 | @skip_test = false 11 | @skip_error = true 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/command_service_object/failure.rb: -------------------------------------------------------------------------------- 1 | module CommandServiceObject 2 | class Failure < StandardError 3 | attr_reader :command 4 | 5 | def initialize(message: nil, command: {}) 6 | @command = command 7 | super(message) 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/command_service_object/helpers/check_helper.rb: -------------------------------------------------------------------------------- 1 | module CommandServiceObject 2 | module CheckHelper 3 | def check!(message, &block) 4 | raise "No block given" unless block_given? 5 | 6 | fail!(message) if block.call 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/command_service_object/helpers/controller_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'active_support/all' 4 | 5 | module CommandServiceObject 6 | module ControllerHelper 7 | def self.included(base) 8 | base.extend ClassMethods 9 | end 10 | 11 | ActiveSupport.on_load :action_controller do 12 | define_method(CommandServiceObject.configuration.command_method_name) \ 13 | do |service: nil, usecase: action_name| 14 | service_name = service || self.class.default_service || controller_name 15 | 16 | "#{service_name}/commands/#{usecase}".camelize.constantize 17 | end 18 | end 19 | 20 | def execute(cmd) 21 | ApplicationService.call(cmd) 22 | end 23 | 24 | module ClassMethods 25 | @default_service = nil 26 | 27 | def default_service(service_name = nil) 28 | @default_service ||= service_name 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/command_service_object/helpers/failure_helper.rb: -------------------------------------------------------------------------------- 1 | module CommandServiceObject 2 | module FailureHelper 3 | def fail!(message) 4 | raise Failure, message: message, command: cmd 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/command_service_object/helpers/model_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CommandServiceObject 4 | module ModelHelper 5 | def model_name 6 | name.camelize 7 | end 8 | 9 | def model_class 10 | Object.const_get(model_name) 11 | rescue StandardError 12 | nil 13 | end 14 | 15 | def model_attributes 16 | default_attr = { REPLACE_ME: String } 17 | return default_attr if model_class.nil? || model_class.try(:columns_hash).nil? 18 | 19 | attrs = {} 20 | 21 | model_class.columns_hash.each do |k, v| 22 | next if ignored_column_names.include?(k) 23 | 24 | type = allowed_column_types[v.type] 25 | next if type.nil? 26 | 27 | attrs[k] = type 28 | end 29 | 30 | attrs 31 | end 32 | 33 | def ignored_column_names 34 | %w[ 35 | created_at 36 | updated_at 37 | encrypted_password 38 | ] 39 | end 40 | 41 | def allowed_column_types 42 | { 43 | string: 'String', 44 | bigint: 'Integer', 45 | integer: 'Integer', 46 | decimal: 'Float', 47 | boolean: 'Boolean', 48 | datetime: 'DateTime' 49 | } 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/command_service_object/hooks.rb: -------------------------------------------------------------------------------- 1 | module CommandServiceObject 2 | module Hooks 3 | def self.included(base) 4 | base.send :extend, ClassMethods 5 | base.send :include, InstanceMethods 6 | 7 | class << base 8 | alias_method :_new, :new 9 | 10 | define_method :new do |command| 11 | _new(command).tap do |instance| 12 | instance.send(:setup_micros, _micros) 13 | end 14 | end 15 | end 16 | end 17 | 18 | module InstanceMethods 19 | attr_accessor :_called_micros 20 | 21 | def _called_micros 22 | @_called_micros ||= [] 23 | end 24 | 25 | def rollback_micros 26 | _called_micros.reverse_each(&:rollback) 27 | end 28 | 29 | def setup_micros(micros) 30 | micros.each do |micro| 31 | method_name = micro.name.split('::').last.underscore 32 | 33 | # unrollable micros 34 | define_singleton_method("#{method_name}!") do |cmd| 35 | micro.new(cmd).call 36 | end 37 | 38 | # rollable micros 39 | define_singleton_method(method_name) do |cmd| 40 | obj = micro.new(cmd) 41 | result = obj.call 42 | 43 | _called_micros << obj 44 | result 45 | end 46 | end 47 | end 48 | end 49 | 50 | module ClassMethods 51 | cattr_accessor :_micros 52 | 53 | def _micros 54 | @_micros ||= Set.new([]) 55 | end 56 | 57 | def micros(*names) 58 | service = to_s.split('::') 59 | service.pop 60 | 61 | names.each do |name| 62 | obj = "#{service.join('/')}/micros/#{name}".camelize.constantize 63 | 64 | _micros.add(obj) 65 | end 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/command_service_object/railtie.rb: -------------------------------------------------------------------------------- 1 | module CommandServiceObject 2 | class Railtie < Rails::Railtie 3 | initializer 'configure_rails_initialization' do |_app| 4 | ActiveSupport.on_load :action_controller do 5 | CommandServiceObject::Railtie.setup_action_controller 6 | end 7 | end 8 | 9 | def self.setup_action_controller 10 | ActionController::Base.send :include, ControllerHelper 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/command_service_object/version.rb: -------------------------------------------------------------------------------- 1 | module CommandServiceObject 2 | VERSION = '1.4.1'.freeze 3 | end 4 | -------------------------------------------------------------------------------- /lib/generators/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adham90/command_service_object/62ad71f61e43def0c4ea25c29a82281685a4c37c/lib/generators/.DS_Store -------------------------------------------------------------------------------- /lib/generators/service/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Explain the generator 3 | 4 | Example: 5 | rails generate service Thing 6 | 7 | This will create: 8 | what/will/it/create 9 | -------------------------------------------------------------------------------- /lib/generators/service/command/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Explain the generator 3 | 4 | Example: 5 | rails generate service Thing 6 | 7 | This will create: 8 | what/will/it/create 9 | -------------------------------------------------------------------------------- /lib/generators/service/command/command_generator.rb: -------------------------------------------------------------------------------- 1 | require_relative '../setup/setup_generator.rb' 2 | require_relative '../helper' 3 | 4 | module Service 5 | module Generators 6 | class CommandGenerator < Rails::Generators::NamedBase 7 | include CommandServiceObject::ModelHelper 8 | 9 | source_root File.expand_path('templates', __dir__) 10 | 11 | argument :commands, type: :array, default: [], banner: 'command command' 12 | 13 | def call 14 | @model_attributes = model_attributes 15 | 16 | commands.each do |c| 17 | @command = c.classify 18 | create_main(c) 19 | create_test(c) 20 | end 21 | end 22 | 23 | private 24 | 25 | def create_main(m) 26 | path = "#{service_path}/commands/#{m.underscore}.rb" 27 | template 'command.rb.erb', path 28 | end 29 | 30 | def create_test(m) 31 | path = "#{spec_path}/commands/#{m.underscore}_spec.rb" 32 | template 'command_spec.rb.erb', path 33 | end 34 | 35 | def service_name 36 | Service::Helper.service_name(name) 37 | end 38 | 39 | def service_path 40 | Service::Helper.service_path(name) 41 | end 42 | 43 | def spec_path 44 | Service::Helper.spec_path(name) 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/generators/service/command/templates/command.rb.erb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module <%= service_name.classify %>::Commands 4 | class <%= @command %> < CommandBase 5 | # You can read Virtus gem doc for more info. 6 | # https://github.com/solnic/virtus 7 | 8 | # Attributes 9 | <%- @model_attributes.each do |k,v| -%> 10 | # attribute :<%=k%>, <%=v%> 11 | <%- end -%> 12 | 13 | # Validations 14 | # validates :REPLACE_ME, presence: true 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/generators/service/command/templates/command_spec.rb.erb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | describe <%= service_name.classify %>::Commands::<%= @command %>, type: :model do 6 | describe 'attrubites' do 7 | <%# it { expect(subject).to respond_to(:EX) } %> 8 | end 9 | 10 | describe 'validatios' do 11 | <%# it { should validate_presence_of(:EX) } %> 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/generators/service/entity/entity_generator.rb: -------------------------------------------------------------------------------- 1 | require_relative '../setup/setup_generator.rb' 2 | require_relative '../helper' 3 | 4 | module Service 5 | module Generators 6 | class EntityGenerator < Rails::Generators::NamedBase 7 | source_root File.expand_path('templates', __dir__) 8 | 9 | argument :entities, type: :array, default: [], banner: 'entities entities' 10 | 11 | def create_micros 12 | entities.each do |m| 13 | @entity = m.classify 14 | 15 | path = "#{service_path}/entities/#{m.underscore}.rb" 16 | template 'entity.rb.erb', path 17 | end 18 | end 19 | 20 | private 21 | 22 | def service_name 23 | Service::Helper.service_name(name) 24 | end 25 | 26 | def service_path 27 | Service::Helper.service_path(name) 28 | end 29 | 30 | def spec_path 31 | Service::Helper.spec_path(name) 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/generators/service/entity/templates/entity.rb.erb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module <%= service_name.classify %>::Entities 4 | class <%= @entity %> 5 | include Virtus.model(nullify_blank: true) 6 | 7 | # attribute :name, String 8 | end 9 | end -------------------------------------------------------------------------------- /lib/generators/service/external/external_generator.rb: -------------------------------------------------------------------------------- 1 | require_relative '../setup/setup_generator.rb' 2 | require_relative '../helper' 3 | 4 | module Service 5 | module Generators 6 | class ExternalGenerator < Rails::Generators::NamedBase 7 | source_root File.expand_path('templates', __dir__) 8 | 9 | argument :externals, type: :array, default: [], banner: 'external external' 10 | 11 | def create_externals 12 | externals.each do |m| 13 | @external = m.classify 14 | create_main(m) 15 | create_test(m) 16 | end 17 | end 18 | 19 | private 20 | 21 | def create_main(m) 22 | path = "#{service_path}/externals/#{m.underscore}.rb" 23 | template 'external.rb.erb', path 24 | end 25 | 26 | def create_test(m) 27 | path = "#{spec_path}/externals/#{m.underscore}_spec.rb" 28 | template 'external_spec.rb.erb', path 29 | end 30 | 31 | def service_name 32 | Service::Helper.service_name(name) 33 | end 34 | 35 | def service_path 36 | Service::Helper.service_path(name) 37 | end 38 | 39 | def spec_path 40 | Service::Helper.spec_path(name) 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/generators/service/external/templates/external.rb.erb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module <%= service_name.classify %>::Externals 4 | module <%= @external %> 5 | extend self 6 | 7 | # Example 8 | # def balance(id) 9 | # res = http.get("user/:id") 10 | # res_as_json = JSON.parse(res.body) 11 | # 12 | # The return value should only be OpenStruct or Hash Object 13 | # OpenStruct.new(user_id: res_as_json[:id], balance: res_as_json[:balance]) 14 | # end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/generators/service/external/templates/external_spec.rb.erb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | describe <%= service_name.classify %>::Externals::<%= @external %> do 6 | # Example 7 | # context 'when success' do 8 | # it 'should be okay?' do 9 | # expect(subject.[method_name]).to eql([result]) 10 | # end 11 | # end 12 | end 13 | -------------------------------------------------------------------------------- /lib/generators/service/external/templates/external_test.rb.erb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adham90/command_service_object/62ad71f61e43def0c4ea25c29a82281685a4c37c/lib/generators/service/external/templates/external_test.rb.erb -------------------------------------------------------------------------------- /lib/generators/service/helper.rb: -------------------------------------------------------------------------------- 1 | module Service 2 | module Helper 3 | extend self 4 | 5 | def service_name(arg) 6 | if arg.include? "/" 7 | root = arg.split("/").first 8 | sub_domain = arg.split("/").last 9 | "#{root.underscore}_service/#{sub_domain}" 10 | else 11 | "#{arg.underscore}_service" 12 | end 13 | end 14 | 15 | def service_path(arg) 16 | "app/services/#{service_name(arg)}" 17 | end 18 | 19 | def spec_path(arg) 20 | "spec/services/#{service_name(arg)}" 21 | end 22 | end 23 | end -------------------------------------------------------------------------------- /lib/generators/service/install/install_generator.rb: -------------------------------------------------------------------------------- 1 | require 'rails/generators' 2 | 3 | module Service 4 | module Generators 5 | class InstallGenerator < Rails::Generators::Base 6 | source_root File.expand_path('templates', __dir__) 7 | 8 | def create_initializer_file 9 | template 'initializer.rb', config_file_path 10 | end 11 | 12 | def create_services_dir 13 | directory 'services', 'app/services' 14 | end 15 | 16 | private 17 | 18 | def config_file_path 19 | 'config/initializers/command_service_object.rb' 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/generators/service/install/templates/initializer.rb: -------------------------------------------------------------------------------- 1 | require 'hutch' 2 | 3 | Hutch::Logging.logger = Rails.logger 4 | Hutch.connect 5 | 6 | CommandServiceObject.configure do |config| 7 | # By setting the append_controller_helper to false you will need to 8 | # manually include the CommandServiceObject::ServiceControllerHelper 9 | # module to your base controller. 10 | # config.append_controller_helper = true 11 | # 12 | # The default command method name is :command 13 | # but you can easily rename it to avoid collisions. 14 | # config.command_method_name = :command 15 | # 16 | # Skip the generation of commands by the rails g service command 17 | # config.skip_command = false 18 | # 19 | # Skip the generation of minitest/rspec by the rails g service command 20 | # config.skip_test = false 21 | # 22 | # Skip the generation of errors by the rails g service command 23 | # config.skip_error = true 24 | end 25 | -------------------------------------------------------------------------------- /lib/generators/service/install/templates/services/application_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationService 4 | class << self 5 | attr_reader :cmd, :usecase, :bm, :result 6 | 7 | def call(cmd) 8 | @cmd = cmd 9 | @bm = Benchmark.measure do 10 | raise Errors::InvalidCommand if cmd.invalid? 11 | 12 | @usecase = usecase_class.new(cmd) 13 | 14 | raise Errors::NotAuthorizedError unless usecase.allowed? 15 | 16 | @result = ServiceResult.new { usecase.call } 17 | 18 | rollback if result.error.present? 19 | usecase.broadcast if result.ok? 20 | end 21 | 22 | log_command 23 | result 24 | rescue StandardError => e 25 | ServiceResult.new { raise e } 26 | end 27 | 28 | def rollback 29 | usecase.rollback_micros 30 | usecase.rollback 31 | end 32 | 33 | private 34 | 35 | def log_command 36 | FileUtils.mkdir_p 'log/services' 37 | service_logger = ActiveSupport::Logger.new( 38 | Rails.root.join('log', 'services', "#{service_name.underscore}.log").to_s, 'daily' 39 | ) 40 | service_logger.formatter = proc do |_severity, datetime, _progname, msg| 41 | "[#{msg['usecase']}] [#{msg['status']}] [#{datetime.to_s(:db)} ##{Process.pid}] -- #{msg['body']}\n" 42 | end 43 | log_body = result.ok? ? success_log : failure_log 44 | 45 | service_logger.info(log_body) 46 | end 47 | 48 | def success_log 49 | { 50 | usecase: "#{service_name}::#{usecase_name}", 51 | status: 'success', 52 | body: { 53 | cmd: cmd.as_json, 54 | result: result.value!.as_json, 55 | benchmark: bm.as_json 56 | } 57 | }.as_json 58 | end 59 | 60 | def failure_log 61 | { 62 | usecase: "#{service_name}::#{usecase_name}", 63 | status: 'faild', 64 | body: { 65 | cmd: cmd.as_json, 66 | error: result.error.to_s, 67 | benchmark: bm.as_json 68 | } 69 | }.as_json 70 | end 71 | 72 | def usecase_class 73 | cmd.class.name.gsub('Commands', 'Usecases').constantize 74 | end 75 | 76 | def service_name 77 | cmd.class.name.split('::').first 78 | end 79 | 80 | def usecase_name 81 | cmd.class.name.split('::').last 82 | end 83 | end 84 | end 85 | 86 | module Errors 87 | class NotAuthorizedError < StandardError 88 | def initialize(msg: 'not allowd') 89 | super(msg) 90 | end 91 | end 92 | 93 | class InvalidCommand < StandardError; end 94 | end 95 | -------------------------------------------------------------------------------- /lib/generators/service/install/templates/services/case_base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'hutch' 4 | 5 | class CaseBase 6 | include CommandServiceObject::Hooks 7 | include CommandServiceObject::FailureHelper 8 | include CommandServiceObject::CheckHelper 9 | 10 | attr_reader :cmd, :issuer, :right_name 11 | alias_attribute :payload, :cmd 12 | 13 | def initialize(cmd) 14 | @cmd = cmd 15 | @issuer = cmd.try(:issuer) 16 | @right_name = "#{service_name}.#{case_name}" 17 | end 18 | 19 | def case_name 20 | self.class.name.split('::').last.downcase 21 | end 22 | 23 | def service_name 24 | self.class.name.split('::').first.remove('Service').downcase 25 | end 26 | 27 | def allowed? 28 | true 29 | end 30 | 31 | def rollback; end 32 | end 33 | -------------------------------------------------------------------------------- /lib/generators/service/install/templates/services/command_base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CommandBase 4 | include ActiveModel::Validations 5 | include Virtus.model 6 | 7 | # if you need to enable policies add issuer attribute 8 | attribute :issuer, Issuer, default: :default_issuer 9 | 10 | private 11 | 12 | def default_issuer 13 | nil 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/generators/service/install/templates/services/issuer.rb: -------------------------------------------------------------------------------- 1 | class Issuer 2 | include ActiveModel::Validations 3 | include Virtus.model 4 | 5 | attribute :id, String 6 | attribute :type, String 7 | attribute :scope, Array[String], default: [] 8 | 9 | validates :id, :type, presence: true 10 | end 11 | -------------------------------------------------------------------------------- /lib/generators/service/install/templates/services/listener_base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'hutch' 4 | 5 | class ListenerBase < SimpleDelegator 6 | include Hutch::Consumer 7 | end -------------------------------------------------------------------------------- /lib/generators/service/install/templates/services/query_base.rb: -------------------------------------------------------------------------------- 1 | require "active_record" 2 | 3 | class QueryBase 4 | RelationRequired = Class.new(StandardError) 5 | 6 | def initialize(*args) 7 | @options = args.extract_options! 8 | @relation = args.first || base_relation 9 | 10 | if relation.nil? 11 | raise( 12 | RelationRequired, 13 | "Queries require a base relation defined. Use .queries method to define relation." 14 | ) 15 | elsif !relation.is_a?(ActiveRecord::Relation) 16 | raise( 17 | RelationRequired, 18 | "Queries accept only ActiveRecord::Relation as input" 19 | ) 20 | end 21 | end 22 | 23 | def self.call(*args) 24 | new(*args).query 25 | end 26 | 27 | def self.queries(subject) 28 | self.base_relation = subject 29 | end 30 | 31 | def base_relation 32 | return nil if self.class.base_relation.nil? 33 | 34 | if self.class.base_relation.is_a?(ActiveRecord::Relation) 35 | self.class.base_relation 36 | elsif self.class.base_relation < ActiveRecord::Base 37 | self.class.base_relation.all 38 | end 39 | end 40 | 41 | private 42 | 43 | class << self 44 | attr_accessor :base_relation 45 | end 46 | 47 | attr_reader :relation, :options 48 | 49 | def query 50 | raise( 51 | NotImplementedError, 52 | "You need to implement #query method which returns ActiveRecord::Relation object" 53 | ) 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/generators/service/install/templates/services/service_result.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ServiceResult 4 | attr_reader :error 5 | 6 | def initialize 7 | @value = yield 8 | @error = nil 9 | rescue StandardError => e 10 | @error = e 11 | end 12 | 13 | def to_s 14 | if ok? 15 | format('#', object_id, @value.inspect) 16 | else 17 | format('#', object_id, @error.inspect) 18 | end 19 | end 20 | 21 | alias inspect to_s 22 | 23 | def then 24 | return self unless ok? 25 | 26 | result = yield(@value) 27 | raise TypeError, 'block invoked in Result#then did not return Result' unless result.is_a?(Result) 28 | 29 | result 30 | end 31 | 32 | def rescue 33 | return self if ok? 34 | 35 | result = yield(@error) 36 | raise TypeError, 'block invoked in Result#rescue did not return Result' unless result.is_a?(Result) 37 | 38 | result 39 | end 40 | 41 | def value 42 | raise ArgumentError, 'must provide a block to Result#value to be invoked in case of error' unless block_given? 43 | 44 | return @value if ok? 45 | 46 | yield(@error) 47 | end 48 | 49 | def value! 50 | raise @error unless ok? 51 | 52 | @value 53 | end 54 | 55 | def ok? 56 | !@error 57 | end 58 | 59 | def self.error(err) 60 | result = allocate 61 | result.instance_variable_set(:@error, err) 62 | result 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/generators/service/install/templates/services/util/json.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | class Util::Json < Virtus::Attribute 4 | def coerce(value) 5 | value.is_a?(::Hash) ? value : JSON.parse(value) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/generators/service/install/templates/services/value_object_base.rb: -------------------------------------------------------------------------------- 1 | class ValueObjectBase 2 | include ActiveModel::Validations 3 | include Virtus.value_object 4 | end 5 | -------------------------------------------------------------------------------- /lib/generators/service/listener/listener_generator.rb: -------------------------------------------------------------------------------- 1 | require_relative '../setup/setup_generator.rb' 2 | require_relative '../helper' 3 | 4 | module Service 5 | module Generators 6 | class ListenerGenerator < Rails::Generators::NamedBase 7 | source_root File.expand_path('templates', __dir__) 8 | 9 | argument :listeners, type: :array, default: [], banner: 'Listener Listener' 10 | 11 | def create_micros 12 | listeners.each do |m| 13 | @listener = m.classify 14 | 15 | path = "#{service_path}/listeners/#{m.underscore}.rb" 16 | template 'listener.rb.erb', path 17 | end 18 | end 19 | 20 | private 21 | 22 | def service_name 23 | Service::Helper.service_name(name) 24 | end 25 | 26 | def service_path 27 | Service::Helper.service_path(name) 28 | end 29 | 30 | def spec_path 31 | Service::Helper.spec_path(name) 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/generators/service/listener/templates/listener.rb.erb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module <%= service_name.classify %>::Listeners 4 | class <%= @listener %> < ListenerBase 5 | consume 'channel name' 6 | 7 | def process(message) 8 | # Logic 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/generators/service/micro/micro_generator.rb: -------------------------------------------------------------------------------- 1 | require_relative '../setup/setup_generator.rb' 2 | require_relative '../helper' 3 | 4 | module Service 5 | module Generators 6 | class MicroGenerator < Rails::Generators::NamedBase 7 | source_root File.expand_path('templates', __dir__) 8 | 9 | argument :micros, type: :array, default: [], banner: 'micros micros' 10 | 11 | def create_micros 12 | micros.each do |m| 13 | @micro = m.classify 14 | 15 | path = "#{service_path}/usecases/micros/#{m.underscore}.rb" 16 | template 'micro.rb.erb', path 17 | end 18 | end 19 | 20 | private 21 | 22 | def service_name 23 | Service::Helper.service_name(name) 24 | end 25 | 26 | def service_path 27 | Service::Helper.service_path(name) 28 | end 29 | 30 | def spec_path 31 | Service::Helper.spec_path(name) 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/generators/service/micro/templates/micro.rb.erb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module <%= service_name.classify %>::Usecases::Micros 4 | class <%= @micro %> < CaseBase 5 | def call 6 | # 7 | end 8 | 9 | def rollback 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/generators/service/query/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | This will generatet query_object under [service_name]/queries 3 | 4 | Example: 5 | rails generate service:query [service name] [query name] 6 | 7 | This will create: 8 | services/[service_name]/queries/[query_name].rb 9 | -------------------------------------------------------------------------------- /lib/generators/service/query/query_generator.rb: -------------------------------------------------------------------------------- 1 | require_relative '../setup/setup_generator.rb' 2 | require_relative '../helper' 3 | 4 | module Service 5 | module Generators 6 | class QueryGenerator < Rails::Generators::NamedBase 7 | source_root File.expand_path('templates', __dir__) 8 | 9 | argument :queries, type: :array, default: [], banner: 'query query' 10 | 11 | def create_queries 12 | queries.each do |m| 13 | @query = m.classify 14 | 15 | path = "#{service_path}/queries/#{m.underscore}.rb" 16 | template 'query.rb.erb', path 17 | end 18 | end 19 | 20 | private 21 | 22 | def service_name 23 | Service::Helper.service_name(name) 24 | end 25 | 26 | def service_path 27 | Service::Helper.service_path(name) 28 | end 29 | 30 | def spec_path 31 | Service::Helper.spec_path(name) 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/generators/service/query/templates/query.rb.erb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module <%= service_name.classify %>::Queries 4 | class <%= @query %> < QueryBase 5 | # queries UserService::Models::User 6 | 7 | # Example 8 | # def query 9 | # map_rows(relation.where(phone: options.fetch(:phone))) 10 | # end 11 | 12 | private 13 | 14 | # def map_rows(rows) 15 | # rows.map { |r| UserService::Views::User.new(r.as_json).to_hash } 16 | # end 17 | 18 | # def map_row(r) 19 | # UserService::Views::User.new(r.as_json).to_hash 20 | # end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/generators/service/service_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rubygems' 4 | require_relative './setup/setup_generator.rb' 5 | require_relative './usecase/usecase_generator.rb' 6 | require_relative './command/command_generator.rb' 7 | require 'rails/generators' 8 | require 'rails/generators/model_helpers' 9 | require_relative './helper.rb' 10 | 11 | module Service 12 | module Generators 13 | class ServiceGenerator < Rails::Generators::NamedBase 14 | argument :usecases, type: :array, default: [], banner: 'usecase usecase' 15 | 16 | def install_if_not 17 | return if File.exist?('app/services') 18 | 19 | generate 'service:install' 20 | end 21 | 22 | def setup 23 | invoke Service::Generators::SetupGenerator, [name] 24 | end 25 | 26 | def generate_usecases 27 | invoke Service::Generators::UsecaseGenerator, [name, usecases] 28 | end 29 | 30 | def generate_commands 31 | invoke Service::Generators::CommandGenerator, [name, usecases] 32 | end 33 | 34 | private 35 | 36 | def service_name 37 | Service::Helper.service_name(name) 38 | end 39 | 40 | def service_path 41 | Service::Helper.service_path(name) 42 | end 43 | 44 | def spec_path 45 | Service::Helper.spec_path(name) 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/generators/service/setup/setup_generator.rb: -------------------------------------------------------------------------------- 1 | require_relative '../helper' 2 | 3 | module Service 4 | module Generators 5 | class SetupGenerator < Rails::Generators::Base 6 | source_root File.expand_path('templates', __dir__) 7 | 8 | DIRS = %w(listeners jobs externals queries usecases commands entities usecases/micros) 9 | 10 | def setup 11 | DIRS.each do |dir| 12 | empty_directory("#{service_path}/#{dir}") 13 | end 14 | 15 | path = "#{service_path}/doc.md" 16 | template 'doc.md.erb', path unless File.exist?(path) 17 | end 18 | 19 | private 20 | 21 | def service_name 22 | Service::Helper.service_name(args.first) 23 | end 24 | 25 | def service_path 26 | Service::Helper.service_path(args.first) 27 | end 28 | 29 | def spec_path 30 | Service::Helper.spec_path(args.first) 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/generators/service/setup/templates/doc.md.erb: -------------------------------------------------------------------------------- 1 | ## <%= service_name.classify %> 2 | The authorization system is responsible for identifying a particular user. 3 | 4 | ### Database Tables 5 | - users 6 | - profiles 7 | 8 | ### Entities: 9 | ###### *Many objects are not fundamentally defined by their attributes, but rather by a thread of continuity and identity.* 10 | - User 11 | - name: string 12 | - email: string 13 | - phone: string 14 | - Profile: 15 | - address: string 16 | - foo: boolean 17 | - bar: int 18 | 19 | ### Usecases (Processes): 20 | ###### *Use Cases focus on Users, Actions, and Processes.* 21 | - **[Login](login-uml-and-full-doc)** - the process for authorization, we write out to the user an authentication key for a month. Only a user with a verified phone and email address can authenticate. 22 | - **[Logout](logout-uml-and-full-doc)** - After logging out, the user will need to log in again to access the system. 23 | 24 | ### Queries: 25 | - FindByEmail(email: string) 26 | - FindByName(name: string) 27 | 28 | ### Listeners: 29 | ###### *An event listener that waits for an event outside the service to occur.* 30 | - order_status 31 | 32 | ### Externals: 33 | ###### *Wrapper for any external interactions.* 34 | - **UserService** 35 | - user_info(id: id) 36 | - **Stripe** - payment gateway wrapper. 37 | - charge(pid, amount, currency) 38 | - refund(charge_id) 39 | - create_customer(email, phone, name) -------------------------------------------------------------------------------- /lib/generators/service/usecase/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Explain the generator 3 | 4 | Example: 5 | rails generate service Thing 6 | 7 | This will create: 8 | what/will/it/create 9 | -------------------------------------------------------------------------------- /lib/generators/service/usecase/templates/usecase.rb.erb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module <%= service_name.classify %>::Usecases 4 | class <%= @usecase %> < CaseBase 5 | # 6 | # Your business logic goes here, keep [call] method clean by using private 7 | # methods for Business logic. 8 | # 9 | def call 10 | # Use check! for business logic validation, 11 | # If the given block returns false then it will raise a business logic failure with the given message. 12 | # Ex: check!("user should not have any active cards") { cmd.issuer.active_cards.empty? } 13 | 14 | replace_me 15 | 16 | output 17 | end 18 | 19 | def output 20 | # return entity object 21 | end 22 | 23 | # This method will run if call method raise error 24 | def rollback 25 | # rollback logic 26 | end 27 | 28 | def allowed? 29 | # policies loginc for issuer 30 | # ex: 31 | # 32 | # return false if issuer.role != :admin 33 | 34 | true 35 | end 36 | 37 | def broadcast 38 | Hutch.publish('<%= service_name %>', action: '<%= "#{@usecase.underscore}" %>', subject: {}) 39 | end 40 | 41 | private 42 | 43 | def replace_me 44 | # [business logic] 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/generators/service/usecase/templates/usecase_spec.rb.erb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails_helper' 4 | 5 | describe <%= service_name.classify %>::Usecases::<%= @usecase %> do 6 | let(:cmd) { <%= service_name.classify %>::Commands::<%= @usecase %>.new(cmd_params) } 7 | let(:cmd_default_params) {{ name: 'test_name' }} 8 | 9 | context 'when success' do 10 | let(:cmd_params) { cmd_default_params } 11 | let!(:result) { ApplicationService.call(cmd) } 12 | 13 | it 'should be okay?' do 14 | expect(result.ok?).to be true 15 | end 16 | end 17 | 18 | context 'when failure' do 19 | let(:cmd_params) { cmd_default_params.merge!({ name: 'invalid_name' }) } 20 | let!(:result) { ApplicationService.call(cmd) } 21 | 22 | it 'should not be okay?' do 23 | expect(result.ok?).to be false 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/generators/service/usecase/usecase_generator.rb: -------------------------------------------------------------------------------- 1 | require_relative '../setup/setup_generator.rb' 2 | require_relative '../helper' 3 | 4 | module Service 5 | module Generators 6 | class UsecaseGenerator < Rails::Generators::NamedBase 7 | source_root File.expand_path('templates', __dir__) 8 | 9 | argument :usecases, type: :array, default: [], banner: 'usecase usecase' 10 | 11 | def call 12 | usecases.each do |u| 13 | @usecase = u.classify 14 | create_main(u) 15 | create_test(u) 16 | end 17 | end 18 | 19 | private 20 | 21 | def create_main(m) 22 | path = "#{service_path}/usecases/#{m.underscore}.rb" 23 | template 'usecase.rb.erb', path 24 | end 25 | 26 | def create_test(m) 27 | path = "#{spec_path}/usecases/#{m.underscore}_spec.rb" 28 | template 'usecase_spec.rb.erb', path 29 | end 30 | 31 | def service_name 32 | Service::Helper.service_name(name) 33 | end 34 | 35 | def service_path 36 | Service::Helper.service_path(name) 37 | end 38 | 39 | def spec_path 40 | Service::Helper.spec_path(name) 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/generators/service/value_object/templates/value_object.rb.erb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module <%= service_name.classify %>::ValueObjects 4 | class <%= @value_object %> < ValueObjectBase 5 | # You can read Virtus gem doc for more info. 6 | # https://github.com/solnic/virtus 7 | 8 | # Attributes 9 | # values do 10 | # attribute :latitude, Float 11 | # attribute :longitude, Float 12 | # end 13 | 14 | # Validations 15 | # validates :REPLACE_ME, presence: true 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/generators/service/value_object/value_object_generator.rb: -------------------------------------------------------------------------------- 1 | require_relative '../setup/setup_generator.rb' 2 | require_relative '../helper' 3 | 4 | module Service 5 | module Generators 6 | class ValueObjectGenerator < Rails::Generators::NamedBase 7 | source_root File.expand_path('templates', __dir__) 8 | 9 | argument :value_objects, type: :array, default: [], banner: 'value_object value_object' 10 | 11 | def call 12 | value_objects.each do |u| 13 | @value_object = u.classify 14 | create_main(u) 15 | end 16 | end 17 | 18 | private 19 | 20 | def create_main(m) 21 | path = "#{service_path}/value_objects/#{m.underscore}.rb" 22 | template 'value_object.rb.erb', path 23 | end 24 | 25 | def service_name 26 | Service::Helper.service_name(name) 27 | end 28 | 29 | def service_path 30 | Service::Helper.service_path(name) 31 | end 32 | 33 | def spec_path 34 | Service::Helper.spec_path(name) 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/command_service_object_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class CommandServiceObjectTest < Minitest::Test 4 | # def test_that_it_has_a_version_number 5 | # refute_nil ::CommandServiceObject::VERSION 6 | # end 7 | 8 | # def test_it_does_something_useful 9 | # assert false 10 | # end 11 | end 12 | -------------------------------------------------------------------------------- /test/generators/install_generator_test.rb: -------------------------------------------------------------------------------- 1 | require 'rails/generators/testing/behaviour' 2 | require 'test_helper' 3 | require 'byebug' 4 | require_relative '../../lib/generators/service/install/install_generator' 5 | 6 | class InstallGeneratorTest < Rails::Generators::TestCase 7 | include GeneratorTestHelpers 8 | tests Service::Generators::InstallGenerator 9 | 10 | destination File.expand_path('../tmp', __dir__) 11 | 12 | create_generator_sample_app 13 | 14 | Minitest.after_run do 15 | remove_generator_sample_app 16 | end 17 | 18 | setup do 19 | run_generator 20 | end 21 | 22 | test 'user_service files creation' do 23 | assert_file 'app/services/application_service.rb' 24 | assert_file 'app/services/case_base.rb' 25 | assert_file 'app/services/service_result.rb' 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/generators/usecase_generator_test.rb: -------------------------------------------------------------------------------- 1 | require 'rails/generators/testing/behaviour' 2 | require 'test_helper' 3 | require 'byebug' 4 | require_relative '../../lib/generators/service/usecase/usecase_generator' 5 | 6 | class UsecaseGeneratorTest < Rails::Generators::TestCase 7 | include GeneratorTestHelpers 8 | tests Service::Generators::UsecaseGenerator 9 | 10 | destination File.expand_path('../tmp', __dir__) 11 | 12 | create_generator_sample_app 13 | 14 | Minitest.after_run do 15 | remove_generator_sample_app 16 | end 17 | 18 | setup do 19 | run_generator %w[auth login] 20 | end 21 | 22 | test 'user_service files creation' do 23 | assert_file 'app/services/auth_service/usecases/login.rb' 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/support/generator_test_helpers.rb: -------------------------------------------------------------------------------- 1 | module GeneratorTestHelpers 2 | def self.included(base) 3 | base.extend ClassMethods 4 | end 5 | 6 | module ClassMethods 7 | def test_path 8 | File.join(File.dirname(__FILE__), '..') 9 | end 10 | 11 | def create_generator_sample_app 12 | FileUtils.cd(test_path) do 13 | system 'rails new tmp --skip-git --skip-active-record --skip-test-unit --skip-spring --skip-bundle --quiet' 14 | end 15 | end 16 | 17 | def remove_generator_sample_app 18 | FileUtils.rm_rf(destination_root) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../lib', __dir__) 2 | require 'command_service_object' 3 | 4 | require 'minitest/autorun' 5 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } 6 | --------------------------------------------------------------------------------