├── lib ├── generators │ └── hmvc_rails │ │ ├── templates │ │ ├── views │ │ │ ├── view.haml.tt │ │ │ ├── view.slim.tt │ │ │ └── view.erb.tt │ │ ├── forms │ │ │ ├── form.rb.tt │ │ │ └── application_form.rb.tt │ │ ├── operations │ │ │ ├── operation.rb.tt │ │ │ └── application_operation.rb.tt │ │ ├── validators │ │ │ ├── email_validator.rb.tt │ │ │ └── uniqueness_validator.rb.tt │ │ ├── controllers │ │ │ └── controller.rb.tt │ │ ├── extras │ │ │ ├── error_response.rb.tt │ │ │ ├── error_resource.rb.tt │ │ │ └── exception.rb.tt │ │ └── configures │ │ │ ├── hmvc_rails_api.rb.tt │ │ │ └── hmvc_rails.rb.tt │ │ ├── install_generator.rb │ │ └── hmvc_rails_generator.rb ├── hmvc │ ├── rails │ │ └── version.rb │ └── rails.rb └── rubocop │ └── cop │ ├── hmvc_rails_cops.rb │ └── hmvc_rails │ ├── formal_style.rb │ └── operating_style.rb ├── CHANGELOG.md ├── hmvc-rails-1.0.0.gem ├── hmvc-rails-1.0.1.gem ├── hmvc-rails-1.0.2.gem ├── hmvc-rails-1.0.3.gem ├── hmvc-rails-1.0.4.gem ├── .gitignore ├── bin ├── setup ├── hmvc └── console ├── sig └── hmvc │ └── rails.rbs ├── Rakefile ├── Gemfile ├── .rubocop.yml ├── test ├── test_configuration.rb ├── test_install_generator.rb └── test_hmvc_rails_generator.rb ├── hmvc-rails.gemspec ├── LICENSE.txt ├── Gemfile.lock ├── CODE_OF_CONDUCT.md └── README.md /lib/generators/hmvc_rails/templates/views/view.haml.tt: -------------------------------------------------------------------------------- 1 | %h1= @_action.camelize 2 | -------------------------------------------------------------------------------- /lib/generators/hmvc_rails/templates/views/view.slim.tt: -------------------------------------------------------------------------------- 1 | h1= @_action.camelize 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [Unreleased] 2 | 3 | ## [1.0.0] - 2023-02-15 4 | 5 | - Initial release 6 | -------------------------------------------------------------------------------- /lib/generators/hmvc_rails/templates/views/view.erb.tt: -------------------------------------------------------------------------------- 1 |

<%= @_action.camelize %>

2 | -------------------------------------------------------------------------------- /hmvc-rails-1.0.0.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TOMOSIA-VIETNAM/hmvc-rails/HEAD/hmvc-rails-1.0.0.gem -------------------------------------------------------------------------------- /hmvc-rails-1.0.1.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TOMOSIA-VIETNAM/hmvc-rails/HEAD/hmvc-rails-1.0.1.gem -------------------------------------------------------------------------------- /hmvc-rails-1.0.2.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TOMOSIA-VIETNAM/hmvc-rails/HEAD/hmvc-rails-1.0.2.gem -------------------------------------------------------------------------------- /hmvc-rails-1.0.3.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TOMOSIA-VIETNAM/hmvc-rails/HEAD/hmvc-rails-1.0.3.gem -------------------------------------------------------------------------------- /hmvc-rails-1.0.4.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TOMOSIA-VIETNAM/hmvc-rails/HEAD/hmvc-rails-1.0.4.gem -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | -------------------------------------------------------------------------------- /lib/hmvc/rails/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Hmvc 4 | module Rails 5 | VERSION = "1.0.4" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/rubocop/cop/hmvc_rails_cops.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "hmvc_rails/operating_style" 4 | require_relative "hmvc_rails/formal_style" 5 | -------------------------------------------------------------------------------- /sig/hmvc/rails.rbs: -------------------------------------------------------------------------------- 1 | module Hmvc 2 | module Rails 3 | VERSION: String 4 | # See the writing guide of rbs: https://github.com/ruby/rbs#guides 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/generators/hmvc_rails/templates/forms/form.rb.tt: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | <%= add_file_traces %> 4 | class <%= class_name %>::<%= @_action.humanize %>Form < <%= Hmvc::Rails.configuration.parent_form %> 5 | end 6 | -------------------------------------------------------------------------------- /lib/generators/hmvc_rails/templates/operations/operation.rb.tt: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | <%= add_file_traces %> 4 | class <%= class_name %>::<%= @_action.humanize %>Operation < <%= Hmvc::Rails.configuration.parent_operation %> 5 | def call; end 6 | end 7 | -------------------------------------------------------------------------------- /bin/hmvc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "#{Dir.pwd}/config/environment" 5 | require "hmvc/rails" 6 | require_relative "../lib/generators/hmvc_rails/hmvc_rails_generator" 7 | 8 | Hmvc::Rails.configuration 9 | HmvcRailsGenerator.start(ARGV) 10 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rubocop/rake_task" 5 | require "rake/testtask" 6 | 7 | RuboCop::RakeTask.new 8 | 9 | Rake::TestTask.new do |t| 10 | t.libs << "test" 11 | end 12 | 13 | task default: :rubocop 14 | task default: :test 15 | -------------------------------------------------------------------------------- /lib/generators/hmvc_rails/templates/operations/application_operation.rb.tt: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | <%= add_file_traces %> 4 | class ApplicationOperation 5 | attr_reader :params, :current_user 6 | 7 | def initialize(params, data = {}) 8 | @params = params 9 | @current_user = data[:current_user] 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/generators/hmvc_rails/templates/validators/email_validator.rb.tt: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | <%= add_file_traces %> 4 | class EmailValidator < ActiveModel::EachValidator 5 | def validate_each(record, attribute, value) 6 | return if value.blank? || /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i.match?(value) 7 | 8 | record.errors.add(attribute, :invalid) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/generators/hmvc_rails/templates/controllers/controller.rb.tt: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | <%= add_file_traces %> 4 | class <%= class_name %>Controller < <%= options[:parent_controller] %><% options[:action].each do |action| %> 5 | <%= path_notes(action) %> 6 | def <%= action %> 7 | operator = <%= class_name %>::<%= action.humanize %>Operation.new(params) 8 | operator.call 9 | end 10 | <% end %>end 11 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "hmvc/rails" 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | # (If you use this, don't forget to add pry to your Gemfile!) 11 | # require "pry" 12 | # Pry.start 13 | 14 | require "irb" 15 | IRB.start(__FILE__) 16 | -------------------------------------------------------------------------------- /lib/rubocop/cop/hmvc_rails/formal_style.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RuboCop 4 | module Cop 5 | module HmvcRails 6 | class FormalStyle < RuboCop::Cop::Base 7 | def on_class(node) 8 | class_name = node.children.first.children.last.to_s 9 | return if class_name.end_with?("Form") 10 | 11 | add_offense(node, message: "The form name does not match the desired format") 12 | end 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/generators/hmvc_rails/templates/extras/error_response.rb.tt: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | <%= add_file_traces %> 4 | module Extras 5 | module ErrorResponse 6 | extend ActiveSupport::Concern 7 | 8 | included do 9 | rescue_from StandardError do |exception| 10 | handle(exception) 11 | end 12 | 13 | private 14 | 15 | def handle(exception) 16 | error = ErrorResource.new(exception) 17 | render json: error.as_json, status: error.status 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | # Specify your gem's dependencies in hmvc-rails.gemspec 6 | gemspec 7 | 8 | group :test do 9 | # Ruby on Rails is a full-stack web framework optimized for programmer happiness and sustainable productivity 10 | gem "rails", "7.0" 11 | 12 | # Rake is a Make-like program implemented in Ruby 13 | gem "rake", "13.0" 14 | 15 | # RuboCop is a Ruby code style checking and code formatting tool 16 | gem "rubocop", "1.21" 17 | 18 | # Minitest provides a complete suite of testing facilities supporting TDD, BDD, mocking, and benchmarking 19 | gem "minitest", "5.17" 20 | end 21 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | SuggestExtensions: false 3 | 4 | Style/StringLiterals: 5 | Enabled: true 6 | EnforcedStyle: double_quotes 7 | 8 | Style/StringLiteralsInInterpolation: 9 | Enabled: true 10 | EnforcedStyle: double_quotes 11 | 12 | Layout/LineLength: 13 | Max: 120 14 | 15 | Style/Documentation: 16 | Enabled: false 17 | 18 | Metrics/CyclomaticComplexity: 19 | Enabled: false 20 | 21 | Metrics/MethodLength: 22 | Enabled: false 23 | 24 | Metrics/ClassLength: 25 | Enabled: false 26 | 27 | Metrics/AbcSize: 28 | Enabled: false 29 | 30 | Gemspec/RequiredRubyVersion: 31 | Enabled: false 32 | 33 | Metrics/PerceivedComplexity: 34 | Enabled: false 35 | -------------------------------------------------------------------------------- /test/test_configuration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "minitest/autorun" 4 | require_relative "../lib/hmvc/rails" 5 | 6 | class TestConfiguration < Minitest::Test 7 | def test_configuration_properties_are_set_correctly 8 | configuration = Hmvc::Rails::Configuration.new 9 | 10 | assert_equal "ApplicationController", configuration.parent_controller 11 | assert_equal %w[index show new create edit update destroy], configuration.action 12 | assert_equal %w[index show new edit], configuration.view 13 | assert_equal %w[new create edit update], configuration.form 14 | assert_equal "ApplicationForm", configuration.parent_form 15 | assert_equal "ApplicationOperation", configuration.parent_operation 16 | assert_equal true, configuration.file_traces 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/generators/hmvc_rails/templates/forms/application_form.rb.tt: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | <%= add_file_traces %> 4 | class ApplicationForm 5 | extend ActiveModel::Translation 6 | 7 | include ActiveModel::Model 8 | include ActiveModel::Attributes 9 | include ActiveModel::Validations::Callbacks 10 | 11 | def initialize(attributes = {}) 12 | self.class.attribute_names.each do |column| 13 | self.class.attribute(column.to_sym) 14 | end 15 | 16 | super attributes 17 | end 18 | 19 | def valid! 20 | raise ExceptionError::UnprocessableEntity, error_messages.to_json unless valid? 21 | end 22 | 23 | private 24 | 25 | def error_messages 26 | errors.messages.map { |key, value| { key => value.first } } 27 | end 28 | 29 | def attribute_names 30 | [] 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/generators/hmvc_rails/templates/extras/error_resource.rb.tt: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | <%= add_file_traces %> 4 | module Extras 5 | class ErrorResource 6 | include Exception 7 | 8 | attr_reader :status, :code, :message 9 | 10 | def initialize(exception) 11 | hash = get_status_code(exception) 12 | @code = hash[:code] 13 | @status = hash[:status] 14 | @message = exception.message 15 | end 16 | 17 | def as_json 18 | return { code: code, field_error: field_errors } if status == UNPROCESSABLE_ENTITY[:status] 19 | 20 | { code: code, message: message } 21 | end 22 | 23 | private 24 | 25 | def field_errors 26 | JSON.parse(message).map do |messages| 27 | messages.map { |field, message_detail| { name: field, message: message_detail } } 28 | end.flatten 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /hmvc-rails.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/hmvc/rails/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "hmvc-rails" 7 | spec.version = Hmvc::Rails::VERSION 8 | spec.authors = ["thucpt"] 9 | spec.email = ["thuc.phan@tomosia.com"] 10 | spec.summary = "hmvc-rails is a high-level model for the Rails MVC architecture" 11 | spec.description = "hmvc-rails is a high-level model for the Rails MVC architecture" 12 | spec.homepage = "https://github.com/TOMOSIA-VIETNAM/hmvc-rails" 13 | spec.license = "MIT" 14 | 15 | spec.files = Dir.chdir(__dir__) do 16 | `git ls-files -z`.split("\x0").reject do |f| 17 | (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)}) 18 | end 19 | end 20 | 21 | spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } 22 | spec.executables << "hmvc" 23 | spec.require_paths = ["lib"] 24 | end 25 | -------------------------------------------------------------------------------- /lib/hmvc/rails.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "rails/version" 4 | 5 | module Hmvc 6 | module Rails 7 | class Error < StandardError; end 8 | 9 | class << self 10 | attr_writer :configuration 11 | 12 | def configuration 13 | @configuration ||= Configuration.new 14 | end 15 | 16 | def configure 17 | yield(configuration) 18 | end 19 | end 20 | 21 | class Configuration 22 | attr_accessor :parent_controller, :action, :view, :form, :parent_form, :parent_operation, :file_traces 23 | 24 | def initialize 25 | @parent_controller = "ApplicationController" 26 | @action = %w[index show new create edit update destroy] 27 | @view = %w[index show new edit] 28 | @form = %w[new create edit update] 29 | @parent_form = "ApplicationForm" 30 | @parent_operation = "ApplicationOperation" 31 | @file_traces = true 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/generators/hmvc_rails/templates/validators/uniqueness_validator.rb.tt: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | <%= add_file_traces %> 4 | class UniquenessValidator < ActiveModel::EachValidator 5 | def initialize(klass) 6 | super 7 | @klass = options[:model] if options[:model] 8 | end 9 | 10 | # rubocop:disable Metrics/AbcSize 11 | def validate_each(record, attribute) 12 | record_org = record 13 | attribute_org = attribute 14 | attribute = options[:attribute].to_sym if options[:attribute] 15 | base_attrs = {} 16 | base_attrs[attribute] = record_org.send(attribute) 17 | 18 | options[:scope]&.each do |scope_attribute| 19 | base_attrs[scope_attribute] = record_org.send(scope_attribute) 20 | end 21 | 22 | record = if record_org.try(:record) 23 | options[:model].where.not(id: record_org.send(:record).id).exists?(base_attrs) 24 | else 25 | options[:model].exists?(base_attrs) 26 | end 27 | 28 | record_org.errors.add(attribute_org, :taken) if record 29 | end 30 | # rubocop:enable Metrics/AbcSize 31 | end 32 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 thucpt 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 | -------------------------------------------------------------------------------- /lib/generators/hmvc_rails/templates/configures/hmvc_rails_api.rb.tt: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | <%= add_file_traces %> 4 | if Rails.env.development? 5 | Hmvc::Rails.configure do |config| 6 | # The controller files's parent class of controller. Default is ApplicationController 7 | # config.parent_controller = "ApplicationController" 8 | 9 | # Method when creating the controller files. Default is %w[index show new create edit update destroy] 10 | config.action = %w[index show create update destroy] 11 | 12 | # Method when creating the view files. Default is %w[index show new edit] 13 | config.view = %w[] 14 | 15 | # The form files's parent class. Default is ApplicationForm 16 | # config.parent_form = "ApplicationForm" 17 | 18 | # Method when creating the form files. Default is %w[new create edit update] 19 | config.form = %w[create update] 20 | 21 | # The operation files's parent class. Default is ApplicationOperation 22 | # config.parent_operation = "ApplicationOperation" 23 | 24 | # Save author name and timestamp to file. Default is true 25 | # config.file_traces = true 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/generators/hmvc_rails/templates/configures/hmvc_rails.rb.tt: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | <%= add_file_traces %> 4 | if Rails.env.development? 5 | Hmvc::Rails.configure do |config| 6 | # The controller files's parent class of controller. Default is ApplicationController 7 | # config.parent_controller = "ApplicationController" 8 | 9 | # Method when creating the controller files. Default is %w[index show new create edit update destroy] 10 | # config.action = %w[index show new create edit update destroy] 11 | 12 | # Method when creating the view files. Default is %w[index show new edit] 13 | # config.view = %w[index show new edit] 14 | 15 | # The form files's parent class. Default is ApplicationForm 16 | # config.parent_form = "ApplicationForm" 17 | 18 | # Method when creating the form files. Default is %w[new create edit update] 19 | # config.form = %w[new create edit update] 20 | 21 | # The operation files's parent class. Default is ApplicationOperation 22 | # config.parent_operation = "ApplicationOperation" 23 | 24 | # Save author name and timestamp to file. Default is true 25 | # config.file_traces = true 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/test_install_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "fileutils" 4 | require "rails/generators/test_case" 5 | require "generators/hmvc_rails/install_generator" 6 | 7 | class TestInstallGenerator < Rails::Generators::TestCase 8 | tests HmvcRails::Generators::InstallGenerator 9 | 10 | def setup 11 | @tmp_dir = Dir.mktmpdir("hmvc_rails_test") 12 | FileUtils.cd(@tmp_dir) 13 | end 14 | 15 | def teardown 16 | FileUtils.cd("/") 17 | FileUtils.rm_rf(@tmp_dir) 18 | end 19 | 20 | def test_install_generator 21 | run_generator 22 | 23 | assert_file "config/initializers/hmvc_rails.rb" 24 | assert_file "app/operations/application_operation.rb" 25 | assert_file "app/forms/application_form.rb" 26 | assert_file "app/validators/uniqueness_validator.rb" 27 | assert_file "app/validators/email_validator.rb" 28 | end 29 | 30 | def test_api_install_generator 31 | run_generator ["--api"] 32 | 33 | assert_file "config/initializers/hmvc_rails.rb" 34 | assert_file "app/operations/application_operation.rb" 35 | assert_file "app/forms/application_form.rb" 36 | assert_file "app/validators/uniqueness_validator.rb" 37 | assert_file "app/validators/email_validator.rb" 38 | assert_file "lib/hmvc_rails/extras/exception.rb" 39 | assert_file "lib/hmvc_rails/extras/error_resource.rb" 40 | assert_file "lib/hmvc_rails/extras/error_response.rb" 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/rubocop/cop/hmvc_rails/operating_style.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RuboCop 4 | module Cop 5 | module HmvcRails 6 | class OperatingStyle < RuboCop::Cop::Base 7 | def on_class(node) 8 | class_name = node.children.first.children.last.to_s 9 | return if class_name.end_with?("Operation") 10 | 11 | add_offense(node, message: "The operation name does not match the desired format") 12 | end 13 | 14 | def on_def(node) 15 | return unless node.children.first == :call 16 | 17 | node.body.to_a.compact.each do |ast| 18 | next if special_node?(ast.class) 19 | 20 | next if ast.is_a?(Symbol) && valid?(ast.to_s) 21 | 22 | next if ast.is_a?(RuboCop::AST::SendNode) && valid?(ast.children.last.to_s) 23 | 24 | add_offense(node, message: "Method works in \"call\" without prefix \"step_\"") 25 | end 26 | end 27 | 28 | private 29 | 30 | def special_node?(node_name) 31 | [ 32 | RuboCop::AST::Node, 33 | RuboCop::AST::IfNode, 34 | RuboCop::AST::IntNode, 35 | RuboCop::AST::ArgsNode, 36 | RuboCop::AST::BlockNode, 37 | RuboCop::AST::YieldNode, 38 | RuboCop::AST::SuperNode, 39 | RuboCop::AST::ReturnNode, 40 | RuboCop::AST::ResbodyNode 41 | ].include?(node_name) 42 | end 43 | 44 | def valid?(text) 45 | %w[step_ transaction].any? { |keyword| text.start_with?(keyword) } 46 | end 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/generators/hmvc_rails/templates/extras/exception.rb.tt: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | <%= add_file_traces %> 4 | module Extras 5 | module Exception 6 | BAD_REQUEST = { status: 400, code: :bad_request }.freeze 7 | UNAUTHORIZED = { status: 401, code: :unauthorized }.freeze 8 | FORBIDDEN = { status: 403, code: :forbidden }.freeze 9 | NOT_FOUND = { status: 404, code: :not_found }.freeze 10 | CONFLICT = { status: 409, code: :conflict }.freeze 11 | UNPROCESSABLE_ENTITY = { status: 422, code: :unprocessable_entity }.freeze 12 | INTERNAL_SERVER_ERROR = { status: 500, code: :internal_server_error }.freeze 13 | NOT_IMPLEMENTED = { status: 501, code: :not_implemented }.freeze 14 | SERVICE_UNAVAILABLE = { status: 503, code: :service_unavailable }.freeze 15 | 16 | def get_status_code(exception) 17 | case exception 18 | when Exception::BadRequest, Exception::ResourceInvalid, ActionController::BadRequest 19 | BAD_REQUEST 20 | when Exception::SecurityError, Exception::Unauthorized 21 | UNAUTHORIZED 22 | when Exception::Forbidden, ActionController::InvalidAuthenticityToken 23 | FORBIDDEN 24 | when ActiveRecord::RecordNotFound 25 | NOT_FOUND 26 | when ActiveRecord::RecordNotUnique 27 | CONFLICT 28 | when Exception::UnprocessableEntity 29 | UNPROCESSABLE_ENTITY 30 | when Exception::NotImplemented 31 | NOT_IMPLEMENTED 32 | else 33 | INTERNAL_SERVER_ERROR 34 | end 35 | end 36 | 37 | class BadRequest < StandardError; end 38 | 39 | class ResourceInvalid < StandardError; end 40 | 41 | class SecurityError < StandardError; end 42 | 43 | class Unauthorized < StandardError; end 44 | 45 | class Forbidden < StandardError; end 46 | 47 | class NotFound < StandardError; end 48 | 49 | class ResourceNotFound < StandardError; end 50 | 51 | class Conflict < StandardError; end 52 | 53 | class UnprocessableEntity < StandardError; end 54 | 55 | class InternalServerError < StandardError; end 56 | 57 | class NotImplemented < StandardError; end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/generators/hmvc_rails/install_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails" 4 | require "rails/generators" 5 | 6 | module HmvcRails 7 | module Generators 8 | class InstallGenerator < Rails::Generators::Base 9 | desc "Generate base files of the HMVC structure" 10 | 11 | class_option :api, type: :boolean, default: false 12 | 13 | source_root File.expand_path("templates", __dir__) 14 | 15 | def generate 16 | copy_configuration 17 | copy_application_operator 18 | copy_application_form 19 | copy_validator 20 | copy_api_error_handler 21 | add_config_api_error_response 22 | add_api_error_response 23 | end 24 | 25 | private 26 | 27 | def copy_configuration 28 | file_selected = options[:api] ? "configures/hmvc_rails_api.rb" : "configures/hmvc_rails.rb" 29 | template file_selected, "config/initializers/hmvc_rails.rb" 30 | end 31 | 32 | def copy_application_operator 33 | template "operations/application_operation.rb", "app/operations/application_operation.rb" 34 | end 35 | 36 | def copy_application_form 37 | template "forms/application_form.rb", "app/forms/application_form.rb" 38 | end 39 | 40 | def copy_validator 41 | template "validators/uniqueness_validator.rb", "app/validators/uniqueness_validator.rb" 42 | template "validators/email_validator.rb", "app/validators/email_validator.rb" 43 | end 44 | 45 | def copy_api_error_handler 46 | return unless options[:api] 47 | 48 | template "extras/exception.rb", "lib/hmvc_rails/extras/exception.rb" 49 | template "extras/error_resource.rb", "lib/hmvc_rails/extras/error_resource.rb" 50 | template "extras/error_response.rb", "lib/hmvc_rails/extras/error_response.rb" 51 | end 52 | 53 | # rubocop:disable Layout/LineLength 54 | def add_config_api_error_response 55 | return if Rails.root.blank? || !options[:api] || behavior == :revoke 56 | 57 | file_path = Rails.root.join("config", "application.rb") 58 | if File.foreach(file_path).grep(/Application < Rails::Application/).any? 59 | inject_into_file file_path, " # This setting to use the error handler of hmvc-rails\n config.eager_load_paths << Rails.root.join('lib', 'hmvc_rails')\n\n", after: "Application < Rails::Application\n" 60 | else 61 | puts "Warning: The hmvc_rails module could not be automatically loaded because the \"Application < Rails::Application\" flag was not found" 62 | puts "Please manually add `config.eager_load_paths << Rails.root.join('lib', 'hmvc_rails')` to your `config/application.rb` to use hmvc-rails error response" 63 | end 64 | end 65 | 66 | def add_api_error_response 67 | return if Rails.root.blank? || !options[:api] || behavior == :revoke 68 | 69 | file_path = Rails.root.join("app", "controllers", "application_controller.rb") 70 | if File.foreach(file_path).grep(/ApplicationController < ActionController::API/).any? 71 | inject_into_file file_path, " include Extras::ErrorResponse\n", after: "ApplicationController < ActionController::API\n" 72 | else 73 | puts "Warning: The error response module could not be automatically added because the \"ApplicationController < ActionController::API\" flag was not found" 74 | puts "Please manually add `include Extras::ErrorResponse` to your application_controller.rb to use hmvc-rails error response" 75 | end 76 | end 77 | # rubocop:enable Layout/LineLength 78 | 79 | def add_file_traces 80 | "# Created at: #{Time.now.strftime("%Y-%m-%d %H:%M %z")}\n# Creator: #{`git config user.email`}" 81 | end 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | hmvc-rails (1.0.5) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | actioncable (7.0.0) 10 | actionpack (= 7.0.0) 11 | activesupport (= 7.0.0) 12 | nio4r (~> 2.0) 13 | websocket-driver (>= 0.6.1) 14 | actionmailbox (7.0.0) 15 | actionpack (= 7.0.0) 16 | activejob (= 7.0.0) 17 | activerecord (= 7.0.0) 18 | activestorage (= 7.0.0) 19 | activesupport (= 7.0.0) 20 | mail (>= 2.7.1) 21 | actionmailer (7.0.0) 22 | actionpack (= 7.0.0) 23 | actionview (= 7.0.0) 24 | activejob (= 7.0.0) 25 | activesupport (= 7.0.0) 26 | mail (~> 2.5, >= 2.5.4) 27 | rails-dom-testing (~> 2.0) 28 | actionpack (7.0.0) 29 | actionview (= 7.0.0) 30 | activesupport (= 7.0.0) 31 | rack (~> 2.0, >= 2.2.0) 32 | rack-test (>= 0.6.3) 33 | rails-dom-testing (~> 2.0) 34 | rails-html-sanitizer (~> 1.0, >= 1.2.0) 35 | actiontext (7.0.0) 36 | actionpack (= 7.0.0) 37 | activerecord (= 7.0.0) 38 | activestorage (= 7.0.0) 39 | activesupport (= 7.0.0) 40 | globalid (>= 0.6.0) 41 | nokogiri (>= 1.8.5) 42 | actionview (7.0.0) 43 | activesupport (= 7.0.0) 44 | builder (~> 3.1) 45 | erubi (~> 1.4) 46 | rails-dom-testing (~> 2.0) 47 | rails-html-sanitizer (~> 1.1, >= 1.2.0) 48 | activejob (7.0.0) 49 | activesupport (= 7.0.0) 50 | globalid (>= 0.3.6) 51 | activemodel (7.0.0) 52 | activesupport (= 7.0.0) 53 | activerecord (7.0.0) 54 | activemodel (= 7.0.0) 55 | activesupport (= 7.0.0) 56 | activestorage (7.0.0) 57 | actionpack (= 7.0.0) 58 | activejob (= 7.0.0) 59 | activerecord (= 7.0.0) 60 | activesupport (= 7.0.0) 61 | marcel (~> 1.0) 62 | mini_mime (>= 1.1.0) 63 | activesupport (7.0.0) 64 | concurrent-ruby (~> 1.0, >= 1.0.2) 65 | i18n (>= 1.6, < 2) 66 | minitest (>= 5.1) 67 | tzinfo (~> 2.0) 68 | ast (2.4.2) 69 | builder (3.2.4) 70 | concurrent-ruby (1.2.2) 71 | crass (1.0.6) 72 | date (3.3.4) 73 | erubi (1.12.0) 74 | globalid (1.2.1) 75 | activesupport (>= 6.1) 76 | i18n (1.14.1) 77 | concurrent-ruby (~> 1.0) 78 | loofah (2.22.0) 79 | crass (~> 1.0.2) 80 | nokogiri (>= 1.12.0) 81 | mail (2.8.1) 82 | mini_mime (>= 0.1.1) 83 | net-imap 84 | net-pop 85 | net-smtp 86 | marcel (1.0.2) 87 | method_source (1.0.0) 88 | mini_mime (1.1.5) 89 | minitest (5.17.0) 90 | net-imap (0.3.7) 91 | date 92 | net-protocol 93 | net-pop (0.1.2) 94 | net-protocol 95 | net-protocol (0.2.2) 96 | timeout 97 | net-smtp (0.4.0) 98 | net-protocol 99 | nio4r (2.7.0) 100 | nokogiri (1.15.5-x86_64-darwin) 101 | racc (~> 1.4) 102 | parallel (1.24.0) 103 | parser (3.2.2.4) 104 | ast (~> 2.4.1) 105 | racc 106 | racc (1.7.3) 107 | rack (2.2.8) 108 | rack-test (2.1.0) 109 | rack (>= 1.3) 110 | rails (7.0.0) 111 | actioncable (= 7.0.0) 112 | actionmailbox (= 7.0.0) 113 | actionmailer (= 7.0.0) 114 | actionpack (= 7.0.0) 115 | actiontext (= 7.0.0) 116 | actionview (= 7.0.0) 117 | activejob (= 7.0.0) 118 | activemodel (= 7.0.0) 119 | activerecord (= 7.0.0) 120 | activestorage (= 7.0.0) 121 | activesupport (= 7.0.0) 122 | bundler (>= 1.15.0) 123 | railties (= 7.0.0) 124 | rails-dom-testing (2.2.0) 125 | activesupport (>= 5.0.0) 126 | minitest 127 | nokogiri (>= 1.6) 128 | rails-html-sanitizer (1.6.0) 129 | loofah (~> 2.21) 130 | nokogiri (~> 1.14) 131 | railties (7.0.0) 132 | actionpack (= 7.0.0) 133 | activesupport (= 7.0.0) 134 | method_source 135 | rake (>= 12.2) 136 | thor (~> 1.0) 137 | zeitwerk (~> 2.5) 138 | rainbow (3.1.1) 139 | rake (13.0.0) 140 | regexp_parser (2.8.3) 141 | rexml (3.2.6) 142 | rubocop (1.21.0) 143 | parallel (~> 1.10) 144 | parser (>= 3.0.0.0) 145 | rainbow (>= 2.2.2, < 4.0) 146 | regexp_parser (>= 1.8, < 3.0) 147 | rexml 148 | rubocop-ast (>= 1.9.1, < 2.0) 149 | ruby-progressbar (~> 1.7) 150 | unicode-display_width (>= 1.4.0, < 3.0) 151 | rubocop-ast (1.30.0) 152 | parser (>= 3.2.1.0) 153 | ruby-progressbar (1.13.0) 154 | thor (1.3.0) 155 | timeout (0.4.1) 156 | tzinfo (2.0.6) 157 | concurrent-ruby (~> 1.0) 158 | unicode-display_width (2.5.0) 159 | websocket-driver (0.7.6) 160 | websocket-extensions (>= 0.1.0) 161 | websocket-extensions (0.1.5) 162 | zeitwerk (2.6.12) 163 | 164 | PLATFORMS 165 | x86_64-darwin-19 166 | 167 | DEPENDENCIES 168 | hmvc-rails! 169 | minitest (= 5.17) 170 | rails (= 7.0) 171 | rake (= 13.0) 172 | rubocop (= 1.21) 173 | 174 | BUNDLED WITH 175 | 2.3.25 176 | -------------------------------------------------------------------------------- /test/test_hmvc_rails_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "fileutils" 4 | require "rails/generators/test_case" 5 | require "generators/hmvc_rails/hmvc_rails_generator" 6 | 7 | class TestHmvcRailsGenerator < Rails::Generators::TestCase 8 | tests HmvcRailsGenerator 9 | 10 | def setup 11 | @tmp_dir = Dir.mktmpdir("hmvc_rails_test") 12 | FileUtils.cd(@tmp_dir) 13 | end 14 | 15 | def teardown 16 | FileUtils.cd("/") 17 | FileUtils.rm_rf(@tmp_dir) 18 | end 19 | 20 | def test_default_generator 21 | run_generator %w[admin] 22 | assert_file "app/controllers/admin_controller.rb" 23 | assert_file "app/operations/admin/index_operation.rb" 24 | assert_file "app/operations/admin/show_operation.rb" 25 | assert_file "app/operations/admin/new_operation.rb" 26 | assert_file "app/operations/admin/create_operation.rb" 27 | assert_file "app/operations/admin/edit_operation.rb" 28 | assert_file "app/operations/admin/update_operation.rb" 29 | assert_file "app/operations/admin/destroy_operation.rb" 30 | assert_file "app/forms/admin/new_form.rb" 31 | assert_file "app/forms/admin/create_form.rb" 32 | assert_file "app/forms/admin/edit_form.rb" 33 | assert_file "app/forms/admin/update_form.rb" 34 | assert_file "app/views/admin/index.html.erb" 35 | assert_file "app/views/admin/show.html.erb" 36 | assert_file "app/views/admin/new.html.erb" 37 | assert_file "app/views/admin/edit.html.erb" 38 | end 39 | 40 | def test_controller_template_is_generated 41 | run_generator %w[admin] 42 | assert_file "app/controllers/admin_controller.rb", /class AdminController < ApplicationController/ 43 | assert_file "app/controllers/admin_controller.rb", /def index/ 44 | assert_file "app/controllers/admin_controller.rb", /def show/ 45 | assert_file "app/controllers/admin_controller.rb", /def new/ 46 | assert_file "app/controllers/admin_controller.rb", /def edit/ 47 | assert_file "app/controllers/admin_controller.rb", /def create/ 48 | assert_file "app/controllers/admin_controller.rb", /def update/ 49 | assert_file "app/controllers/admin_controller.rb", /def destroy/ 50 | end 51 | 52 | def test_operation_template_is_generated 53 | run_generator %w[admin] 54 | assert_file "app/operations/admin/index_operation.rb", /class Admin::IndexOperation < ApplicationOperation/ 55 | assert_file "app/operations/admin/show_operation.rb", /class Admin::ShowOperation < ApplicationOperation/ 56 | assert_file "app/operations/admin/new_operation.rb", /class Admin::NewOperation < ApplicationOperation/ 57 | assert_file "app/operations/admin/create_operation.rb", /class Admin::CreateOperation < ApplicationOperation/ 58 | assert_file "app/operations/admin/edit_operation.rb", /class Admin::EditOperation < ApplicationOperation/ 59 | assert_file "app/operations/admin/update_operation.rb", /class Admin::UpdateOperation < ApplicationOperation/ 60 | assert_file "app/operations/admin/destroy_operation.rb", /class Admin::DestroyOperation < ApplicationOperation/ 61 | assert_file "app/operations/admin/index_operation.rb", /def call/ 62 | assert_file "app/operations/admin/show_operation.rb", /def call/ 63 | assert_file "app/operations/admin/new_operation.rb", /def call/ 64 | assert_file "app/operations/admin/create_operation.rb", /def call/ 65 | assert_file "app/operations/admin/edit_operation.rb", /def call/ 66 | assert_file "app/operations/admin/update_operation.rb", /def call/ 67 | assert_file "app/operations/admin/destroy_operation.rb", /def call/ 68 | end 69 | 70 | def test_form_template_is_generated 71 | run_generator %w[admin] 72 | assert_file "app/forms/admin/new_form.rb", /class Admin::NewForm < ApplicationForm/ 73 | assert_file "app/forms/admin/create_form.rb", /class Admin::CreateForm < ApplicationForm/ 74 | assert_file "app/forms/admin/edit_form.rb", /class Admin::EditForm < ApplicationForm/ 75 | assert_file "app/forms/admin/update_form.rb", /class Admin::UpdateForm < ApplicationForm/ 76 | end 77 | 78 | def test_form_template_is_generated_when_form_and_action_match 79 | run_generator %w[admin --action=index --form=index] 80 | assert_file "app/forms/admin/index_form.rb" 81 | end 82 | 83 | def test_form_template_is_not_generated_when_form_and_action_mismatch 84 | run_generator %w[admin --action=index --form=show] 85 | assert_no_file "app/forms/admin/index_form.rb" 86 | end 87 | 88 | def test_generator_with_skip_form_option 89 | run_generator %w[admin --action=new --skip-form] 90 | assert_no_file "app/forms/admin/new_form.rb" 91 | end 92 | 93 | def test_generator_with_skip_view_option 94 | run_generator %w[admin --action=new --skip-view] 95 | assert_no_file "app/views/admin/new.html.erb" 96 | end 97 | 98 | def test_view_template_is_generated_when_view_and_action_match 99 | run_generator %w[admin --action=index] 100 | assert_file "app/views/admin/index.html.erb" 101 | end 102 | 103 | def test_view_template_is_not_generated_when_view_and_action_mismatch 104 | run_generator %w[admin --action=create] 105 | assert_no_file "app/views/admin/create.html.erb" 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 8 | 9 | ## Our Standards 10 | 11 | Examples of behavior that contributes to a positive environment for our community include: 12 | 13 | * Demonstrating empathy and kindness toward other people 14 | * Being respectful of differing opinions, viewpoints, and experiences 15 | * Giving and gracefully accepting constructive feedback 16 | * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 17 | * Focusing on what is best not just for us as individuals, but for the overall community 18 | 19 | Examples of unacceptable behavior include: 20 | 21 | * The use of sexualized language or imagery, and sexual attention or 22 | advances of any kind 23 | * Trolling, insulting or derogatory comments, and personal or political attacks 24 | * Public or private harassment 25 | * Publishing others' private information, such as a physical or email 26 | address, without their explicit permission 27 | * Other conduct which could reasonably be considered inappropriate in a 28 | professional setting 29 | 30 | ## Enforcement Responsibilities 31 | 32 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. 33 | 34 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. 35 | 36 | ## Scope 37 | 38 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. 39 | 40 | ## Enforcement 41 | 42 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at thuc.phan@tomosia.com. All complaints will be reviewed and investigated promptly and fairly. 43 | 44 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 45 | 46 | ## Enforcement Guidelines 47 | 48 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: 49 | 50 | ### 1. Correction 51 | 52 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 53 | 54 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 55 | 56 | ### 2. Warning 57 | 58 | **Community Impact**: A violation through a single incident or series of actions. 59 | 60 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 61 | 62 | ### 3. Temporary Ban 63 | 64 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. 65 | 66 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 67 | 68 | ### 4. Permanent Ban 69 | 70 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 71 | 72 | **Consequence**: A permanent ban from any sort of public interaction within the community. 73 | 74 | ## Attribution 75 | 76 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, 77 | available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 78 | 79 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 80 | 81 | [homepage]: https://www.contributor-covenant.org 82 | 83 | For answers to common questions about this code of conduct, see the FAQ at 84 | https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. 85 | -------------------------------------------------------------------------------- /lib/generators/hmvc_rails/hmvc_rails_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails/generators" 4 | 5 | class HmvcRailsGenerator < Rails::Generators::NamedBase 6 | source_root File.expand_path("templates", __dir__) 7 | 8 | class_option :action, type: :array, default: Hmvc::Rails.configuration.action 9 | class_option :form, type: :array, default: Hmvc::Rails.configuration.form 10 | class_option :parent_controller, type: :string, default: Hmvc::Rails.configuration.parent_controller 11 | class_option :skip_form, type: :boolean, default: false 12 | class_option :skip_view, type: :boolean, default: false 13 | 14 | def generate 15 | validate_options 16 | create_controller 17 | create_operation 18 | create_form 19 | create_view 20 | end 21 | 22 | private 23 | 24 | def validate_options 25 | return if behavior == :revoke 26 | 27 | validate_name 28 | validate_params 29 | validate_form_option 30 | validate_action_option 31 | validate_parent_controller_option 32 | validate_skip_form_option 33 | validate_skip_view_option 34 | end 35 | 36 | def create_controller 37 | template "controllers/controller.rb", 38 | File.join("app/controllers", class_path.join("/"), "#{file_name}_controller.rb") 39 | end 40 | 41 | def create_operation 42 | options[:action].each do |action| 43 | @_action = action 44 | template "operations/operation.rb", File.join("app/operations", file_path, "#{action}_operation.rb") 45 | end 46 | end 47 | 48 | def create_form 49 | return if options[:skip_form] || options[:form].blank? 50 | 51 | forms = options[:form] & options[:action] 52 | return puts "Warning: No forms created! Form and action controller does mismatch" if forms.blank? 53 | 54 | forms.each do |action| 55 | @_action = action 56 | template "forms/form.rb", File.join("app/forms", file_path, "#{action}_form.rb") 57 | end 58 | end 59 | 60 | def create_view 61 | return if options[:skip_view] || Hmvc::Rails.configuration.view.blank? 62 | 63 | views = Hmvc::Rails.configuration.view & options[:action] 64 | return puts "Warning: No views created! View and action controller does mismatch" if views.blank? 65 | 66 | view_engine = Rails.application&.config&.generators&.options&.dig(:rails, :template_engine) || "erb" 67 | copy_view_template(views, view_engine) 68 | rescue StandardError 69 | copy_view_template(views, "erb") 70 | end 71 | 72 | def copy_view_template(views, view_engine) 73 | views.each do |action| 74 | @_action = action 75 | template "views/view.#{view_engine}", File.join("app/views", file_path, "#{action}.html.#{view_engine}") 76 | end 77 | end 78 | 79 | def argv 80 | @argv ||= ARGV.map { |arg| arg.split("=") }.flatten 81 | end 82 | 83 | def validate_name 84 | show_error_message("Invalid name arguments '#{argv[1]}'") if argv[1] && !argv[1].start_with?("-") 85 | end 86 | 87 | def validate_params 88 | option_params = argv.select { |arg| arg.include?("-") } 89 | wrong_options = option_params - %w[--action --form --parent-controller --skip-form --skip-view] 90 | show_error_message("Invalid optional arguments '#{wrong_options.join(", ")}'") if wrong_options.present? 91 | end 92 | 93 | def validate_action_option 94 | action_options = argv.select { |arg| arg == "--action" } 95 | show_error_message("Optional '--action' is duplicated") if action_options.size > 1 96 | end 97 | 98 | def validate_form_option 99 | form_options = argv.select { |arg| arg == "--form" } 100 | show_error_message("Optional '--form' is duplicated") if form_options.size > 1 101 | end 102 | 103 | def validate_parent_controller_option 104 | parent_options = argv.select { |arg| arg == "--parent-controller" } 105 | show_error_message("Optional '--parent-controller' is duplicated") if parent_options.size > 1 106 | index = argv.index("--parent-controller") 107 | return unless index 108 | 109 | if argv[index + 1].blank? || argv[index + 1].start_with?("-") || 110 | (argv[index + 2] && !argv[index + 2].start_with?("-")) 111 | show_error_message("Option '--parent-controller' is not valid") 112 | end 113 | end 114 | 115 | def validate_skip_form_option 116 | index = argv.index("--skip-form") 117 | return if index.blank? || argv[index + 1].blank? || argv[index + 1].start_with?("-") 118 | 119 | show_error_message("Option '--skip-form' is not valid") 120 | end 121 | 122 | def validate_skip_view_option 123 | index = argv.index("--skip-view") 124 | return if index.blank? || argv[index + 1].blank? || argv[index + 1].start_with?("-") 125 | 126 | show_error_message("Option '--skip-view' is not valid") 127 | end 128 | 129 | def show_error_message(message) 130 | puts "Error:" 131 | puts " #{message}" 132 | puts "------" 133 | puts `rails g hmvc_rails --help` 134 | exit(1) 135 | end 136 | 137 | def path_notes(action) 138 | case action 139 | when "index" then "# [GET]..." 140 | when "show" then "# [GET]..." 141 | when "new" then "# [GET]..." 142 | when "edit" then "# [GET]..." 143 | when "create" then "# [POST]..." 144 | when "update" then "# [PUT]..." 145 | when "destroy" then "# [DELETE]..." 146 | else "# [METHOD]..." 147 | end 148 | end 149 | 150 | def add_file_traces 151 | return unless Hmvc::Rails.configuration.file_traces 152 | 153 | "# Created at: #{Time.now.strftime("%Y-%m-%d %H:%M %z")}\n# Creator: #{`git config user.email`}" 154 | end 155 | end 156 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HMVC-RAILS 2 | 3 | ## Summary 4 | 5 | - hmvc-rails is the high-level model of MVC (MVC high-level structure) 6 | 7 | - hmvc-rails makes it easy to manage source code and develop projects 8 | 9 | ## Features 10 | 11 | - Generate controller file 12 | - Generate operations file 13 | - Generate forms file 14 | - Generate views file 15 | - Add file creator and creation date 16 | 17 | ## Installation 18 | 19 | Add this line to your application's Gemfile 20 | 21 | ```ruby 22 | group :development do 23 | gem 'hmvc-rails' 24 | end 25 | ``` 26 | 27 | Then execute 28 | 29 | ``` 30 | bundle install 31 | ``` 32 | 33 | And run 34 | 35 | ``` 36 | rails g hmvc_rails:install 37 | ``` 38 | 39 | ``` 40 | create config/initializers/hmvc.rb 41 | create app/operations/application_operation.rb 42 | create app/forms/application_form.rb 43 | create app/validators/uniqueness_validator.rb 44 | create app/validators/email_validator.rb 45 | ``` 46 | 47 | If it's an API project then you can run 48 | 49 | ``` 50 | rails g hmvc_rails:install --api 51 | ``` 52 | 53 | ``` 54 | create config/initializers/hmvc_rails.rb 55 | create app/operations/application_operation.rb 56 | create app/forms/application_form.rb 57 | create app/validators/uniqueness_validator.rb 58 | create app/validators/email_validator.rb 59 | create lib/hmvc_rails/extras/exception.rb 60 | create lib/hmvc_rails/extras/error_resource.rb 61 | create lib/hmvc_rails/extras/error_response.rb 62 | insert config/application.rb 63 | insert app/controllers/application_controller.rb 64 | ``` 65 | 66 | ## Usage 67 | 68 | ### Default generator 69 | 70 | ``` 71 | rails g hmvc_rails controller_name 72 | ``` 73 | 74 | ### Short command 75 | 76 | ``` 77 | hmvc controller_name 78 | ``` 79 | 80 | Example 81 | 82 | ``` 83 | rails g hmvc_rails admin 84 | 85 | --- 86 | 87 | OR 88 | 89 | hmvc admin 90 | ``` 91 | 92 | ``` 93 | create app/controllers/admin_controller.rb 94 | create app/operations/admin/index_operation.rb 95 | create app/operations/admin/show_operation.rb 96 | create app/operations/admin/new_operation.rb 97 | create app/operations/admin/create_operation.rb 98 | create app/operations/admin/edit_operation.rb 99 | create app/operations/admin/update_operation.rb 100 | create app/operations/admin/destroy_operation.rb 101 | create app/forms/admin/new_form.rb 102 | create app/forms/admin/create_form.rb 103 | create app/forms/admin/edit_form.rb 104 | create app/forms/admin/update_form.rb 105 | create app/views/admin/index.html.erb 106 | create app/views/admin/show.html.erb 107 | create app/views/admin/new.html.erb 108 | create app/views/admin/edit.html.erb 109 | ``` 110 | 111 | ### Options (You can also use short command too) 112 | 113 | ##### 1. If you want to create with action other than default. You can use option `--action` 114 | 115 | ``` 116 | rails g hmvc_rails admin --action index show list detail selection 117 | ``` 118 | 119 | ##### 2. If you want to create with form action other than default. You can use option `--form` 120 | 121 | ``` 122 | rails g hmvc_rails admin --action index show list detail selection --form index show list 123 | ``` 124 | 125 | ##### 3. If you want to create with parent controller other than default. You can use option `--parent-controller` 126 | 127 | ``` 128 | rails g hmvc_rails admin --parent-controller PersonController 129 | ``` 130 | 131 | ##### 4. If you want to skip creating the forms file when generate. You can use option `--skip-form` 132 | 133 | ``` 134 | rails g hmvc_rails admin --skip-form 135 | ``` 136 | 137 | Or change configuration `config.form = %w[]` 138 | 139 | ##### 5. If you want to skip creating the views file when generate. You can use option `--skip-view` 140 | 141 | ``` 142 | rails g hmvc_rails admin --skip-view 143 | ``` 144 | 145 | Or change configuration `config.view = %w[]` 146 | 147 | ## Configuration 148 | 149 | If you want to change the default value when creating the file, please uncomment and update 150 | 151 | _config/initializers/hmvc.rb_ 152 | 153 | ```ruby 154 | # frozen_string_literal: true 155 | 156 | # Created at: 2023-02-18 22:30 +0700 157 | # Creator: thuc.phan@tomosia.com 158 | 159 | if Rails.env.development? 160 | Hmvc::Rails.configure do |config| 161 | # The controller files's parent class of controller. Default is ApplicationController 162 | # config.parent_controller = "ApplicationController" 163 | 164 | # Method when creating the controller files. Default is %w[index show new create edit update destroy] 165 | # config.action = %w[index show new create edit update destroy] 166 | 167 | # Method when creating the view files. Default is %w[index show new edit] 168 | # config.view = %w[index show new edit] 169 | 170 | # The form files's parent class. Default is ApplicationForm 171 | # config.parent_form = "ApplicationForm" 172 | 173 | # Method when creating the form files. Default is %w[new create edit update] 174 | # config.form = %w[new create edit update] 175 | 176 | # The operation files's parent class. Default is ApplicationOperation 177 | # config.parent_operation = "ApplicationOperation" 178 | 179 | # Save author name and timestamp to file. Default is true 180 | # config.file_traces = true 181 | end 182 | end 183 | ``` 184 | 185 | ## Rollback generator 186 | 187 | If you want to rollback the hmvc-rails generator. You can run command 188 | 189 | ``` 190 | rails d hmvc_rails controller_name 191 | ``` 192 | 193 | - - - 194 | 195 | ## Test and debug gem on development environment 196 | 197 | - Add gem and run `bundle` (link to gem hmvc-rails project) 198 | 199 | ```ruby 200 | group :development do 201 | gem 'hmvc-rails', path: '../../Projects/hmvc-rails' 202 | gem 'pry' 203 | end 204 | ``` 205 | 206 | - Add `binding.pry` to the line you want to test 207 | 208 | ```ruby 209 | require 'pry' 210 | ... 211 | def create_controller 212 | binding.pry 213 | template "controller.rb", File.join("app/controllers", class_path.join("/"), "#{file_name}_controller.rb") 214 | end 215 | ``` 216 | 217 | - Run command generator at Rails project terminal 218 | 219 | ``` 220 | rails g hmvc_rails admin 221 | ``` 222 | 223 | ``` 224 | 37: def create_controller 225 | => 38: binding.pry 226 | 39: template "controllers/controller.rb", 227 | 40: File.join("app/controllers", class_path.join("/"), "#{file_name}_controller.rb") 228 | 41: end 229 | 230 | [1] pry(#)> options[:action] 231 | => ["index", "show", "new", "create", "edit", "update", "destroy"] 232 | [2] pry(#)> 233 | ``` 234 | 235 | - Code convention check 236 | 237 | ``` 238 | ➜ hmvc-rails git:(main) rubocop 239 | Inspecting 14 files 240 | .............. 241 | 242 | 14 files inspected, no offenses detected 243 | ``` 244 | 245 | - Run unit test 246 | 247 | ``` 248 | ➜ hmvc-rails git:(main) ✗ rake test 249 | Run options: --seed 9122 250 | 251 | # Running: 252 | 253 | ............. 254 | 255 | Finished in 0.721364s, 18.0214 runs/s, 166.3515 assertions/s. 256 | 13 runs, 120 assertions, 0 failures, 0 errors, 0 skips 257 | ``` 258 | 259 | ## Configure rubocop 260 | 261 | If your project used rubocop for code convention. You can add the below configuration for some conventional hmvc-rails 262 | 263 | _.rubocop.yml_ 264 | 265 | ```ruby 266 | require: rubocop/cop/hmvc_rails_cops 267 | 268 | HmvcRails/OperatingStyle: 269 | Enabled: true 270 | Include: 271 | - app/operations/**/*.rb 272 | 273 | HmvcRails/FormalStyle: 274 | Enabled: true 275 | Include: 276 | - app/forms/**/*.rb 277 | ``` 278 | 279 | Example hmvc-rails offenses 280 | 281 | ``` 282 | ➜ demo git:(master) ✗ rubocop 283 | Inspecting 48 files 284 | ......C......C.................................. 285 | 286 | Offenses: 287 | 288 | app/forms/admin/new_form.rb:6:1: C: HmvcRails/FormalStyle: The form filename does not match the desired format 289 | class Admin::NewFormm < ApplicationForm ... 290 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 291 | app/operations/admin/new_operation.rb:6:1: C: HmvcRails/OperatingStyle: The operation filename does not match the desired format 292 | class Admin::NewOperationn < ApplicationOperation ... 293 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 294 | app/operations/admin/new_operation.rb:7:3: C: HmvcRails/OperatingStyle: Method works in "call" without prefix "step_" 295 | def call ... 296 | ^^^^^^^^ 297 | 298 | 48 files inspected, 3 offenses detected 299 | ``` 300 | 301 | ## License 302 | 303 | The gem `hmvc-rails` is copyright TOMOSIA VIET NAM CO., LTD 304 | 305 | ## Contributor 306 | 307 | - Thuc Phan T. thuc.phan@tomosia.com 308 | - Minh Tang Q. minh.tang@tomosia.com 309 | 310 | ## About [TOMOSIA VIET NAM CO., LTD](https://www.tomosia.com/) 311 | 312 | A company that creates new value together with customers and lights the light of happiness 313 | 314 | 【一緒に】【ハッピー】【ライトアップ】 315 | 316 | お客様と共に新たな価値を生み出し幸せの明かりを灯す会社、トモシア 317 | --------------------------------------------------------------------------------