modulorails 0.2.2 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/.dockerignore +14 -0
  3. data/.gitignore +3 -0
  4. data/.rubocop.yml +62 -0
  5. data/.travis.yml +23 -4
  6. data/Appraisals +18 -0
  7. data/CHANGELOG.md +42 -1
  8. data/Dockerfile.ruby25 +34 -0
  9. data/Dockerfile.ruby26 +28 -0
  10. data/Dockerfile.ruby27 +25 -0
  11. data/Dockerfile.ruby30 +25 -0
  12. data/Dockerfile.ruby31 +25 -0
  13. data/Gemfile.lock +59 -54
  14. data/README.md +16 -4
  15. data/Rakefile +1 -1
  16. data/app/assets/stylesheets/modulorails.css +21 -0
  17. data/app/helpers/modulorails/application_helper.rb +8 -0
  18. data/config/locales/en.yml +3 -0
  19. data/docker-compose.debug.yml +47 -0
  20. data/docker-compose.yml +37 -0
  21. data/entrypoints/appraisal_test.sh +7 -0
  22. data/gemfiles/rails_52.gemfile +9 -0
  23. data/gemfiles/rails_60.gemfile +9 -0
  24. data/gemfiles/rails_61.gemfile +9 -0
  25. data/gemfiles/rails_70.gemfile +9 -0
  26. data/lib/generators/modulorails/docker/docker_generator.rb +19 -0
  27. data/lib/generators/modulorails/docker/templates/Dockerfile.prod.tt +57 -0
  28. data/lib/generators/modulorails/docker/templates/Dockerfile.tt +26 -0
  29. data/lib/generators/modulorails/docker/templates/config/database.yml.tt +32 -0
  30. data/lib/generators/modulorails/docker/templates/docker-compose.prod.yml.tt +50 -0
  31. data/lib/generators/modulorails/docker/templates/docker-compose.yml.tt +71 -0
  32. data/lib/generators/modulorails/docker/templates/entrypoints/docker-entrypoint.sh.tt +20 -0
  33. data/lib/generators/modulorails/docker/templates/entrypoints/webpack-entrypoint.sh.tt +7 -0
  34. data/lib/generators/modulorails/gitlabci/gitlabci_generator.rb +34 -0
  35. data/lib/generators/modulorails/gitlabci/templates/.gitlab-ci.yml.tt +118 -0
  36. data/lib/generators/modulorails/gitlabci/templates/.modulorails-gitlab-ci +6 -0
  37. data/lib/generators/modulorails/gitlabci/templates/config/database-ci.yml.tt +8 -0
  38. data/lib/generators/modulorails/healthcheck/health_check_generator.rb +41 -0
  39. data/lib/generators/modulorails/healthcheck/templates/.modulorails-health_check +6 -0
  40. data/lib/generators/modulorails/healthcheck/templates/config/initializers/health_check.rb.tt +100 -0
  41. data/lib/generators/modulorails/rubocop/rubocop_generator.rb +24 -0
  42. data/lib/generators/modulorails/rubocop/templates/rubocop.yml.tt +18 -0
  43. data/lib/generators/modulorails/self_update/self_update_generator.rb +32 -0
  44. data/lib/generators/modulorails/service/service_generator.rb +13 -0
  45. data/lib/generators/modulorails/service/templates/service.rb.tt +28 -0
  46. data/lib/modulorails/configuration.rb +8 -2
  47. data/lib/modulorails/data.rb +21 -2
  48. data/lib/modulorails/error_data.rb +21 -0
  49. data/lib/modulorails/errors/base_error.rb +4 -0
  50. data/lib/modulorails/errors/errors.rb +3 -0
  51. data/lib/modulorails/errors/invalid_format_error.rb +14 -0
  52. data/lib/modulorails/errors/invalid_value_error.rb +14 -0
  53. data/lib/modulorails/railtie.rb +35 -3
  54. data/lib/modulorails/services/base_service.rb +203 -0
  55. data/lib/modulorails/services/logs_for_method_service.rb +42 -0
  56. data/lib/modulorails/services/services.rb +2 -0
  57. data/lib/modulorails/success_data.rb +17 -0
  58. data/lib/modulorails/validators/database_configuration.rb +9 -3
  59. data/lib/modulorails/version.rb +4 -1
  60. data/lib/modulorails.rb +46 -21
  61. data/modulorails.gemspec +4 -0
  62. metadata +114 -17
  63. data/lib/generators/gitlabci_generator.rb +0 -134
  64. data/lib/generators/templates/.gitlab-ci.yml +0 -72
  65. data/lib/generators/templates/.modulorails-gitlab-ci +0 -3
  66. data/lib/generators/templates/config/database-ci.yml +0 -7
  67. data/lib/modulorails/updater.rb +0 -46
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+
5
+ class Modulorails::RubocopGenerator < Rails::Generators::Base
6
+ source_root File.expand_path('templates', __dir__)
7
+ desc 'This generator creates a configuration for Rubocop'
8
+
9
+ def create_config_files
10
+ rubocop_config_path = Rails.root.join('.rubocop.yml')
11
+ gitlab_config_path = Rails.root.join('.gitlab-ci.yml')
12
+
13
+ template "rubocop.yml", rubocop_config_path, force: true
14
+
15
+ unless File.read(gitlab_config_path).match?(/\s+extends:\s+.lint\s*$/)
16
+ append_file gitlab_config_path do
17
+ <<~YAML
18
+ rubocop:
19
+ extends: .lint
20
+ YAML
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,18 @@
1
+ # The behavior of RuboCop can be controlled via the .rubocop.yml
2
+ # configuration file. It makes it possible to enable/disable
3
+ # certain cops (checks) and to alter their behavior if they accept
4
+ # any parameters. The file can be placed either in your home
5
+ # directory or in some project directory.
6
+ #
7
+ # RuboCop will start looking for the configuration file in the directory
8
+ # where the inspected file is and continue its way up to the root directory.
9
+ #
10
+ # See https://docs.rubocop.org/rubocop/configuration
11
+
12
+ inherit_gem:
13
+ modulorails: .rubocop.yml
14
+
15
+ # Take into account the exclude list from the gem
16
+ inherit_mode:
17
+ merge:
18
+ - Exclude
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+
5
+ # Author: Matthieu 'ciappa_m' Ciappara
6
+ # This updates modulorails by editing the gemfile and running a bundle update
7
+ class Modulorails::SelfUpdateGenerator < Rails::Generators::Base
8
+ source_root File.expand_path('templates', __dir__)
9
+ desc 'This generator updates Modulorails if required'
10
+
11
+ LATEST_VERSION_URL = 'https://rubygems.org/api/v1/versions/modulorails/latest.json'.freeze
12
+
13
+ def create_config_file
14
+ modulorails_version = Gem::Version.new(Modulorails::VERSION)
15
+
16
+ # Get the last published version
17
+ last_published_version_s = HTTParty.get(LATEST_VERSION_URL).parsed_response['version']
18
+ last_published_version = Gem::Version.new(last_published_version_s)
19
+
20
+ # Do nothing if we could not fetch the last published version (whatever the reason)
21
+ # Or if the current version is the same as the last published version
22
+ return if last_published_version <= modulorails_version
23
+
24
+ # Add gem to Gemfile
25
+ gsub_file 'Gemfile', /^\s*gem\s['"]modulorails['"].*$/, "gem 'modulorails', '= #{last_published_version}'"
26
+
27
+ # Update the gem and the Gemfile.lock
28
+ system('bundle install')
29
+ rescue StandardError => e
30
+ $stderr.puts("[Modulorails] Error: cannot generate health_check configuration: #{e.message}")
31
+ end
32
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+
5
+ class Modulorails::ServiceGenerator < Rails::Generators::NamedBase
6
+ source_root File.expand_path('templates', __dir__)
7
+ desc 'This generator creates a service inheriting Modulorails::BaseService'
8
+ argument :arguments, type: :array, default: [], banner: 'argument argument'
9
+
10
+ def create_service_files
11
+ template "service.rb", File.join("app/services", class_path, "#{file_name}_service.rb")
12
+ end
13
+ end
@@ -0,0 +1,28 @@
1
+ <% module_namespacing do -%>
2
+ # @author <INSERT YOUR NAME HERE>
3
+ # <DESCRIBE YOUR CLASS HERE>
4
+ <%- if arguments.size >= 1 -%>
5
+ #
6
+ <%- arguments.each do |argument| -%>
7
+ # @!attribute <%= argument %>
8
+ # @return <DESCRIBE YOUR ARGUMENT HERE>
9
+ <%- end -%>
10
+ <%- end -%>
11
+ class <%= class_name %>Service < ::ApplicationService
12
+ <%- if arguments.size >= 1 -%>
13
+
14
+ def initialize(<%= arguments.join(', ') %>)
15
+ super()
16
+
17
+ <%- arguments.each do |argument| -%>
18
+ <%= "@#{argument} = #{argument}" %>
19
+ <%- end -%>
20
+ end
21
+ <%- end -%>
22
+
23
+ def call
24
+ # TODO
25
+ end
26
+
27
+ end
28
+ <% end -%>
@@ -4,7 +4,7 @@ module Modulorails
4
4
  class Configuration
5
5
  # All the keys to configure the gem
6
6
  attr_accessor :_name, :_main_developer, :_project_manager, :_endpoint, :_api_key,
7
- :_no_auto_update
7
+ :_no_auto_update, :_production_url, :_staging_url, :_review_base_url
8
8
 
9
9
  # This allows to define a DSL to configure the gem
10
10
  # Example:
@@ -14,8 +14,14 @@ module Modulorails
14
14
  # config.project_manager 'pm@modulotech.fr'
15
15
  # config.endpoint "intranet's endpoint"
16
16
  # config.api_key "intranet's api key"
17
+ # config.production_url "production.app.com"
18
+ # config.staging_url "staging.app.com"
19
+ # config.review_base_url "review.app.com"
17
20
  # end
18
- %i[name main_developer project_manager endpoint api_key no_auto_update].each do |field|
21
+ %i[
22
+ name main_developer project_manager endpoint api_key no_auto_update production_url staging_url
23
+ review_base_url
24
+ ].each do |field|
19
25
  define_method(field) do |value=nil|
20
26
  # No value means we want to get the field
21
27
  return send("_#{field}") unless value
@@ -10,7 +10,8 @@ module Modulorails
10
10
  # All the data handled by this class
11
11
  ATTRIBUTE_KEYS = %i[
12
12
  name main_developer project_manager repository type rails_name ruby_version rails_version
13
- bundler_version modulorails_version adapter db_version adapter_version
13
+ bundler_version modulorails_version adapter db_version adapter_version production_url
14
+ staging_url review_base_url
14
15
  ].freeze
15
16
 
16
17
  # Useful if the gem's user need to read one of the data
@@ -30,7 +31,7 @@ module Modulorails
30
31
  # Get the gem's specifications to fetch the versions of critical gems
31
32
  loaded_specs = Gem.loaded_specs
32
33
 
33
- # The three data written by the user in the configuration
34
+ # The data written by the user in the configuration
34
35
  # The name is the usual name of the project, the one used in conversations at Modulotech
35
36
  @name = configuration.name
36
37
  # The main developer, the lead developer, in short the developer to call when something's
@@ -39,6 +40,19 @@ module Modulorails
39
40
  # The project manager of the application; the other person to call when something's wrong with
40
41
  # the application ;)
41
42
  @project_manager = configuration.project_manager
43
+ # The URL of the production environment for the application
44
+ @production_url = configuration.production_url
45
+ # The URL of the staging environment for the application
46
+ @staging_url = configuration.staging_url
47
+ # The base URL of the review environment for the application.
48
+ # A real review URL is built like this at Modulotech:
49
+ # https://review-#{shortened_branch_name}-#{ci_slug}.#{review_base_url}
50
+ # Example:
51
+ # review_base_url: dev.app.com
52
+ # branch_name: 786-a_super_branch => shortened_branch_name: 786-a_sup
53
+ # ci_slug: jzzham
54
+ # |-> https://review-786-a_sup-jzzham.dev.app.com/
55
+ @review_base_url = configuration.review_base_url
42
56
 
43
57
  # Theorically, origin is the main repository of the project and git is the sole VCS we use
44
58
  # at Modulotech
@@ -101,6 +115,11 @@ module Modulorails
101
115
  'adapter' => @adapter,
102
116
  'db_version' => @db_version,
103
117
  'gem_version' => @adapter_version
118
+ },
119
+ 'urls' => {
120
+ 'production' => @production_url,
121
+ 'staging' => @staging_url,
122
+ 'review_base' => @review_base_url
104
123
  }
105
124
  }
106
125
  }
@@ -0,0 +1,21 @@
1
+ # @author Matthieu Ciappara <ciappa_m@modulotech>
2
+ # An error encountered during an operation with additional data.
3
+ class Modulorails::ErrorData
4
+ # @!attribute r errors
5
+ # An error message or an array of error messages (those will be joined by a coma and a space).
6
+ # @!attribute r exception
7
+ # The exception that caused the error. Defaults to nil.
8
+ attr_reader :errors, :exception
9
+
10
+ # @param errors [String,Array<String>] An error message or an array of error messages
11
+ # @param exception [Exception,nil] The exception that caused the error.
12
+ def initialize(errors, exception: nil)
13
+ @errors = errors.respond_to?(:join) ? errors.join(', ') : errors.to_s
14
+ @exception = exception
15
+ end
16
+
17
+ # @return [false] An error always means the operation was not a success.
18
+ def success?
19
+ false
20
+ end
21
+ end
@@ -0,0 +1,4 @@
1
+ # @author Matthieu CIAPPARA <ciappa_m@modulotech.fr>
2
+ # The base class for modulorails exceptions.
3
+ class Modulorails::BaseError < RuntimeError
4
+ end
@@ -0,0 +1,3 @@
1
+ require_relative 'base_error'
2
+ require_relative 'invalid_format_error'
3
+ require_relative 'invalid_value_error'
@@ -0,0 +1,14 @@
1
+ # @author Matthieu CIAPPARA <ciappa_m@modulotech.fr>
2
+ # An exception representing an invalid format for a given field.
3
+ class Modulorails::InvalidFormatError < Modulorails::BaseError
4
+ # @!attribute r field
5
+ # The name of the field that had a wrong format.
6
+ attr_reader :field
7
+
8
+ # @param field [String]
9
+ def initialize(field)
10
+ super(I18n.t('modulorails.errors.invalid_format', field: field))
11
+
12
+ @field = field
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ # @author Matthieu CIAPPARA <ciappa_m@modulotech.fr>
2
+ # An exception representing an invalid value for a given field.
3
+ class Modulorails::InvalidValueError < Modulorails::BaseError
4
+ # @!attribute r field
5
+ # The name of the field that had a wrong value.
6
+ attr_reader :field
7
+
8
+ # @param field [String]
9
+ def initialize(field)
10
+ super(I18n.t('modulorails.errors.invalid_value', field: field))
11
+
12
+ @field = field
13
+ end
14
+ end
@@ -1,9 +1,38 @@
1
+ require_relative '../../app/helpers/modulorails/application_helper'
2
+
1
3
  module Modulorails
4
+ # Bind in the Rails lifecycle
2
5
  class Railtie < ::Rails::Railtie
3
- # Binding in the Rails lifecycle. Sending data after the initialization ensures we can access
4
- # all gems and symbols we might have to use.
6
+ # Update and add gems before we load the configuration
7
+ config.before_configuration do
8
+ # Currently, we limit everything to the development environment
9
+ if Rails.env.development?
10
+ # Check database configuration
11
+ Modulorails.generate_healthcheck_template
12
+ end
13
+ end
14
+
15
+ # Require the gem before we read the health_check initializer
16
+ config.before_initialize do
17
+ require 'health_check'
18
+ end
19
+
20
+ initializer 'modulorails.action_view' do
21
+ ActiveSupport.on_load :action_view do
22
+ include Modulorails::ApplicationHelper
23
+ end
24
+ end
25
+
26
+ initializer 'modulorails.assets' do |app|
27
+ %w[stylesheets javascripts].each do |subdirectory|
28
+ app.config.assets.paths << File.expand_path("../../../app/assets/#{subdirectory}", __FILE__)
29
+ end
30
+ end
31
+
32
+ # Sending data after the initialization ensures we can access
33
+ # all gems, constants and configurations we might need.
5
34
  config.after_initialize do
6
- # For now, we limit everything to the development environment
35
+ # Currently, we limit everything to the development environment
7
36
  if Rails.env.development?
8
37
  # Load translations
9
38
  I18n.load_path += [File.expand_path('../../../config/locales/en.yml', __FILE__)]
@@ -17,6 +46,9 @@ module Modulorails
17
46
  # Check database configuration
18
47
  Modulorails.check_database_config
19
48
 
49
+ # Add/update Rubocop config
50
+ Modulorails.generate_rubocop_template
51
+
20
52
  # Gem's self-update if a new version was released
21
53
  Modulorails.self_update
22
54
  end
@@ -0,0 +1,203 @@
1
+ # @author Matthieu CIAPPARA <ciappa_m@modulotech.fr>
2
+ # The base class for services. Should be implemented by ApplicationService following the model of
3
+ # ActiveRecord::Base and ApplicationRecord.
4
+ class Modulorails::BaseService
5
+ # Allow to instantiate the service and call the service in one go.
6
+ if Modulorails::COMPARABLE_RUBY_VERSION < Gem::Version.new('3.0')
7
+ def self.call(*args, &block)
8
+ new(*args, &block).call
9
+ end
10
+ else
11
+ def self.call(*args, **kwargs, &block)
12
+ new(*args, **kwargs, &block).call
13
+ end
14
+ end
15
+
16
+ # @abstract The main method to implement for your service to do something
17
+ def call
18
+ raise NotImplementedError.new('Implement method call on sub-class')
19
+ end
20
+
21
+ def to_s
22
+ self.class.to_s
23
+ end
24
+
25
+ # The method used by Modulorails::LogsForMethodService to log the service name
26
+ def to_tag
27
+ self.class.to_s
28
+ end
29
+
30
+ # Shamelessly copied from text_helper
31
+ def self.pluralize(count, singular, plural_arg = nil, plural: plural_arg, locale: I18n.locale)
32
+ word = if count == 1 || count =~ /^1(\.0+)?$/
33
+ singular
34
+ else
35
+ plural || singular.pluralize(locale)
36
+ end
37
+
38
+ "#{count || 0} #{word}"
39
+ end
40
+
41
+ protected
42
+
43
+ # Wrapper to Modulorails::LogsForMethodService
44
+ # @param method [#to_s] The method calling `#log`
45
+ # @param message [Hash,#to_s] The message to log; Hash will be logged after a #to_json call
46
+ def log(method, message)
47
+ Modulorails::LogsForMethodService.call(method: method, message: message, tags: [self])
48
+ end
49
+
50
+ # @yield Wrap the given block in an ActiveRecord transaction.
51
+ # @yieldreturn [Object] Will be available as data of the `SuccessData` returned by the method
52
+ # @return [SuccessData] If the transaction was not rollbacked; give access to the block's return.
53
+ # @return [ErrorData] If the transaction was rollbacked; give access to the rollbacking exception.
54
+ def with_transaction
55
+ data = nil
56
+
57
+ ActiveRecord::Base.transaction do
58
+ data = yield
59
+ end
60
+
61
+ SuccessData.new(data)
62
+ rescue ActiveRecord::RecordInvalid => e
63
+ # Known error, no need for a log, it just needs to be returned
64
+ ErrorData.new(e.message, exception: e)
65
+ rescue StandardError => e
66
+ # Unknown error, log the error
67
+ Rails.logger.error("#{self}: #{e.message}")
68
+ Rails.logger.error("Local variables: #{local_variables.map! { |v| { v => eval(v.to_s, binding) } }}")
69
+ Rails.logger.error(e.backtrace&.join("\n"))
70
+
71
+ # Return the error
72
+ ErrorData.new(e.message, exception: e)
73
+ end
74
+
75
+ # Cast the date/datetime parameters to time with zones.
76
+ # @param from [String,ActiveSupport::TimeWithZone] the minimum date
77
+ # @param to [String,ActiveSupport::TimeWithZone] the maximum date
78
+ # @return [[ActiveSupport::TimeWithZone, ActiveSupport::TimeWithZone]] The given dates casted.
79
+ def params_to_time(from, to = nil)
80
+ from = from.is_a?(String) && from.present? ? from.to_time_with_zone : from
81
+ to = if to.is_a?(String) && to.present?
82
+ to = to.to_time_with_zone
83
+
84
+ # If the right bound is exactly the same as the left one, we add 1 day to the right
85
+ # one by default.
86
+ to.present? && to == from ? to + 1.day : to
87
+ else
88
+ to
89
+ end
90
+
91
+ [from, to]
92
+ end
93
+
94
+ # Shamelessly copied from text_helper
95
+ def pluralize(count, singular, plural_arg = nil, plural: plural_arg, locale: I18n.locale)
96
+ self.class.pluralize(count, singular, plural_arg, plural: plural, locale: locale)
97
+ end
98
+
99
+ # Take a series of keys, dig them into the given params and ensure the found value is a date.
100
+ # @param keys [Array<Symbol>] The keys to find in the parameters.
101
+ # @param params [#dig] The parameters to dig in.
102
+ # @return [ActiveSupport::TimeWithZone] If it is found, the value casted as a datetime.
103
+ # @return [nil] If the parameter was not found.
104
+ def parse_date_field(*keys, params:)
105
+ value = params.dig(*keys)
106
+
107
+ return nil unless value
108
+
109
+ begin
110
+ Time.zone.parse(value)
111
+ rescue ArgumentError
112
+ raise InvalidFormatError.new(keys.join('/'))
113
+ end
114
+ end
115
+
116
+ # Take a series of keys, dig them into the given params and ensure the found value is a date.
117
+ # @param keys [Array<Symbol>] The keys to find in the parameters.
118
+ # @param params [#dig] The parameters to dig in.
119
+ # @return [ActiveSupport::TimeWithZone] If it is found, the value casted as a datetime.
120
+ # @return [nil] If the parameter was not found.
121
+ def parse_iso8601_field(*keys, params: {})
122
+ value = params.dig(*keys)
123
+
124
+ return nil unless value
125
+
126
+ begin
127
+ Time.zone.iso8601(value)
128
+ rescue ArgumentError
129
+ raise InvalidFormatError.new(keys.join('/'))
130
+ end
131
+ end
132
+
133
+ # Take a series of keys, dig them into the given params and ensure the found value is valid.
134
+ # @param allowed_values [#include?] Allowed values
135
+ # @param keys [Array<Symbol>] The keys to find in the parameters.
136
+ # @param params [#dig] The parameters to dig in.
137
+ # @param allow_nil [Boolean] Do not raise if value is nil.
138
+ # @return [ActiveSupport::TimeWithZone] If it is found, the value casted as a datetime.
139
+ # @return [nil] If the parameter was not found.
140
+ def parse_enumerated_field(allowed_values, keys, params: {}, allow_nil: true)
141
+ value = params.dig(*keys)
142
+
143
+ if value.respond_to?(:each)
144
+ raise InvalidValueError.new(keys.join('/')) unless value.all? { |v| allowed_values.include?(v) }
145
+ else
146
+ return nil if !value && allow_nil
147
+
148
+ raise InvalidValueError.new(keys.join('/')) unless allowed_values.include?(value)
149
+ end
150
+
151
+ value
152
+ end
153
+
154
+ # @param model [#find_by] The model to search in
155
+ # @param field [Symbol] The field to filter on
156
+ # @param keys [Array<Symbol>] The keys to search in the params
157
+ # @param params [#dig] The params to search in
158
+ # @param allow_nil [Boolean] Raise if the keys are not found; default true
159
+ # @return [#id, nil] The record corresponding to given field and values (through keys)
160
+ # @raise [InvalidValueError] When there is no record for given field and values
161
+ def parse_referential_value(model, field, *keys, params: {}, allow_nil: true)
162
+ value = params.dig(*keys)
163
+
164
+ unless value
165
+ if allow_nil
166
+ return nil
167
+ else
168
+ raise InvalidValueError.new(keys.join('/'))
169
+ end
170
+ end
171
+
172
+ result = model.find_by(field => value)
173
+
174
+ raise InvalidValueError.new(keys.join('/')) if result.nil?
175
+
176
+ result
177
+ end
178
+
179
+ # @param model [#where] The model to search in
180
+ # @param field [Symbol] The field to filter on
181
+ # @param keys [Array<Symbol>] The keys to search in the params
182
+ # @param params [#dig] The parmas to search in
183
+ # @param allow_nil [Boolean] Raise if the keys are not found; default true
184
+ # @return [#ids, nil] The record corresponding to given field and values (through keys)
185
+ # @raise [InvalidValueError] When there is no record for given field and values
186
+ def parse_referential_values(model, field, *keys, params: {}, allow_nil: true)
187
+ values = params.dig(*keys)
188
+
189
+ if values.blank?
190
+ if allow_nil
191
+ return nil
192
+ else
193
+ raise InvalidValueError.new(keys.join('/'))
194
+ end
195
+ end
196
+
197
+ results = model.where(field => values)
198
+
199
+ raise InvalidValueError.new(keys.join('/')) if results.blank?
200
+
201
+ results
202
+ end
203
+ end
@@ -0,0 +1,42 @@
1
+ # @author Matthieu CIAPPARA <ciappa_m@modulotech.fr>
2
+ # A service to write formatted debug logs from a method.
3
+ class Modulorails::LogsForMethodService < Modulorails::BaseService
4
+
5
+ # @param method [String] The name of the calling method
6
+ # @param message [String,#to_json] The body of the log.
7
+ # @param tags [Array<String,#to_tag>] A list of tags to prefix the log.
8
+ def initialize(method:, message:, tags: [])
9
+ super()
10
+ @method = method
11
+ @message = message
12
+ @tags = tags
13
+ end
14
+
15
+ # Write a formatted debug log using given initialization parameters
16
+ def call
17
+ # Map the tags (either objects responding to #to_tag or strings) to prefix the log body
18
+ tag_strings = @tags.map do |tag|
19
+ tag.respond_to?(:to_tag) ? "[#{tag.to_tag}]" : "[#{tag}]"
20
+ end
21
+
22
+ # If the message respond_to #to_json (and is not a String), use it.
23
+ @message = jsonify if !@message.is_a?(String) && @message.respond_to?(:to_json)
24
+
25
+ # Join the tags
26
+ tag_string = tag_strings.join
27
+
28
+ # Split on newlines to avoid a log of a thousand columns and for each line, prefix the tags
29
+ # and log it as debug.
30
+ @message.split("\n").each do |line|
31
+ msg = "#{tag_string}[#{@method}] #{line}"
32
+
33
+ Rails.logger.debug(msg)
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def jsonify
40
+ @message.to_json
41
+ end
42
+ end
@@ -0,0 +1,2 @@
1
+ require_relative 'base_service'
2
+ require_relative 'logs_for_method_service'
@@ -0,0 +1,17 @@
1
+ # @author Matthieu Ciappara <ciappa_m@modulotech>
2
+ # A success resulting from an operation with optional additional data.
3
+ class Modulorails::SuccessData
4
+ # @!attribute r data
5
+ # An object to transport some data (for instance the result of the operation). Defaults to nil.
6
+ attr_reader :data
7
+
8
+ # @param data [Object] An object to transport some data (for instance the result of the operation)
9
+ def initialize(data=nil)
10
+ @data = data
11
+ end
12
+
13
+ # @return [true] A success always means the operation was a success. ;)
14
+ def success?
15
+ true
16
+ end
17
+ end
@@ -31,7 +31,7 @@ module Modulorails
31
31
 
32
32
  def call
33
33
  database_configuration = check_standard_config_file_location
34
- return false unless database_configuration
34
+ return [:standard_config_file_location] unless database_configuration
35
35
 
36
36
  check_test_database_not_equals_dev_database(database_configuration)
37
37
  check_rules_for_environment(database_configuration, :development)
@@ -52,13 +52,19 @@ module Modulorails
52
52
 
53
53
  def check_standard_config_file_location
54
54
  # Load the configuration
55
- config = Psych.load_file(Rails.root.join('config/database.yml'))
55
+ config = if Modulorails::COMPARABLE_RUBY_VERSION >= Gem::Version.new('3.1')
56
+ # Ruby 3.1 uses Psych4 which changes the default way of handling aliases in
57
+ # `load_file`.
58
+ Psych.load_file(Rails.root.join('config/database.yml'), aliases: true)
59
+ else
60
+ Psych.load_file(Rails.root.join('config/database.yml'))
61
+ end
56
62
 
57
63
  # If no exception was raised, then the database configuration file is at standard location
58
64
  @rules[:standard_config_file_location] = true
59
65
 
60
66
  config
61
- rescue StandardError =>e
67
+ rescue StandardError => e
62
68
  # An exception was raised, either the file is not a the standard location, either it just
63
69
  # cannot be read. Either way, we consider the config as invalid
64
70
  @rules[:standard_config_file_location] = false
@@ -1,3 +1,6 @@
1
1
  module Modulorails
2
- VERSION = '0.2.2'
2
+ VERSION = '1.0.1'
3
+
4
+ # Useful to compare the current Ruby version
5
+ COMPARABLE_RUBY_VERSION = Gem::Version.new(RUBY_VERSION)
3
6
  end