much-rails 0.0.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +3 -1
  3. data/lib/much-rails.rb +85 -0
  4. data/lib/much-rails/action.rb +415 -0
  5. data/lib/much-rails/action/base_command_result.rb +22 -0
  6. data/lib/much-rails/action/base_result.rb +14 -0
  7. data/lib/much-rails/action/base_router.rb +474 -0
  8. data/lib/much-rails/action/controller.rb +100 -0
  9. data/lib/much-rails/action/head_result.rb +18 -0
  10. data/lib/much-rails/action/redirect_to_result.rb +18 -0
  11. data/lib/much-rails/action/render_result.rb +25 -0
  12. data/lib/much-rails/action/router.rb +101 -0
  13. data/lib/much-rails/action/send_data_result.rb +18 -0
  14. data/lib/much-rails/action/send_file_result.rb +18 -0
  15. data/lib/much-rails/action/unprocessable_entity_result.rb +37 -0
  16. data/lib/much-rails/assets.rb +54 -0
  17. data/lib/much-rails/boolean.rb +7 -0
  18. data/lib/much-rails/call_method.rb +27 -0
  19. data/lib/much-rails/call_method_callbacks.rb +122 -0
  20. data/lib/much-rails/change_action.rb +83 -0
  21. data/lib/much-rails/change_action_result.rb +59 -0
  22. data/lib/much-rails/config.rb +55 -0
  23. data/lib/much-rails/date.rb +50 -0
  24. data/lib/much-rails/decimal.rb +7 -0
  25. data/lib/much-rails/destroy_action.rb +32 -0
  26. data/lib/much-rails/destroy_service.rb +67 -0
  27. data/lib/much-rails/has_slug.rb +7 -0
  28. data/lib/much-rails/input_value.rb +19 -0
  29. data/lib/much-rails/json.rb +29 -0
  30. data/lib/much-rails/layout.rb +142 -0
  31. data/lib/much-rails/layout/helper.rb +25 -0
  32. data/lib/much-rails/mixin.rb +7 -0
  33. data/lib/much-rails/not_given.rb +7 -0
  34. data/lib/much-rails/rails_routes.rb +29 -0
  35. data/lib/much-rails/railtie.rb +39 -0
  36. data/lib/much-rails/records.rb +5 -0
  37. data/lib/much-rails/records/always_destroyable.rb +30 -0
  38. data/lib/much-rails/records/not_destroyable.rb +30 -0
  39. data/lib/much-rails/records/validate_destroy.rb +92 -0
  40. data/lib/much-rails/result.rb +7 -0
  41. data/lib/much-rails/save_action.rb +32 -0
  42. data/lib/much-rails/save_service.rb +68 -0
  43. data/lib/much-rails/service.rb +18 -0
  44. data/lib/much-rails/service_validation_errors.rb +41 -0
  45. data/lib/much-rails/time.rb +28 -0
  46. data/lib/much-rails/version.rb +3 -1
  47. data/lib/much-rails/view_models.rb +3 -0
  48. data/lib/much-rails/view_models/breadcrumb.rb +11 -0
  49. data/lib/much-rails/wrap_and_call_method.rb +41 -0
  50. data/lib/much-rails/wrap_method.rb +45 -0
  51. data/much-rails.gemspec +20 -4
  52. data/test/helper.rb +20 -2
  53. data/test/support/actions/show.rb +11 -0
  54. data/test/support/config/routes/test.rb +3 -0
  55. data/test/support/factory.rb +2 -0
  56. data/test/support/fake_action_controller.rb +63 -0
  57. data/test/unit/action/base_command_result_tests.rb +43 -0
  58. data/test/unit/action/base_result_tests.rb +22 -0
  59. data/test/unit/action/base_router_tests.rb +530 -0
  60. data/test/unit/action/controller_tests.rb +110 -0
  61. data/test/unit/action/head_result_tests.rb +24 -0
  62. data/test/unit/action/redirect_to_result_tests.rb +24 -0
  63. data/test/unit/action/render_result_tests.rb +43 -0
  64. data/test/unit/action/router_tests.rb +252 -0
  65. data/test/unit/action/send_data_result_tests.rb +24 -0
  66. data/test/unit/action/send_file_result_tests.rb +24 -0
  67. data/test/unit/action/unprocessable_entity_result_tests.rb +51 -0
  68. data/test/unit/action_tests.rb +400 -0
  69. data/test/unit/assets_tests.rb +127 -0
  70. data/test/unit/boolean_tests.rb +17 -0
  71. data/test/unit/call_method_callbacks_tests.rb +176 -0
  72. data/test/unit/call_method_tests.rb +62 -0
  73. data/test/unit/change_action_result_tests.rb +113 -0
  74. data/test/unit/change_action_tests.rb +260 -0
  75. data/test/unit/config_tests.rb +68 -0
  76. data/test/unit/date_tests.rb +55 -0
  77. data/test/unit/decimal_tests.rb +17 -0
  78. data/test/unit/destroy_action_tests.rb +83 -0
  79. data/test/unit/destroy_service_tests.rb +238 -0
  80. data/test/unit/has_slug_tests.rb +17 -0
  81. data/test/unit/input_value_tests.rb +34 -0
  82. data/test/unit/json_tests.rb +55 -0
  83. data/test/unit/layout_tests.rb +155 -0
  84. data/test/unit/mixin_tests.rb +17 -0
  85. data/test/unit/much-rails_tests.rb +82 -4
  86. data/test/unit/not_given_tests.rb +17 -0
  87. data/test/unit/rails_routes_tests.rb +28 -0
  88. data/test/unit/records/always_destroyable_tests.rb +43 -0
  89. data/test/unit/records/not_destroyable_tests.rb +40 -0
  90. data/test/unit/records/validate_destroy_tests.rb +252 -0
  91. data/test/unit/result_tests.rb +17 -0
  92. data/test/unit/save_action_tests.rb +83 -0
  93. data/test/unit/save_service_tests.rb +264 -0
  94. data/test/unit/service_tests.rb +33 -0
  95. data/test/unit/service_validation_errors_tests.rb +107 -0
  96. data/test/unit/time_tests.rb +58 -0
  97. data/test/unit/view_models/breadcrumb_tests.rb +53 -0
  98. data/test/unit/wrap_and_call_method_tests.rb +163 -0
  99. data/test/unit/wrap_method_tests.rb +112 -0
  100. metadata +356 -7
  101. data/test/unit/.keep +0 -0
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "much-rails/action"
4
+ require "much-rails/change_action_result"
5
+ require "much-rails/config"
6
+ require "much-rails/mixin"
7
+
8
+ module MuchRails; end
9
+
10
+ module MuchRails::ChangeAction
11
+ Error = Class.new(StandardError)
12
+
13
+ include MuchRails::Mixin
14
+
15
+ mixin_included do
16
+ include MuchRails::Config
17
+ include MuchRails::Action
18
+
19
+ add_config :much_rails_change_action
20
+
21
+ on_after_call do
22
+ if any_unextracted_change_result_validation_errors?
23
+ raise(
24
+ MuchRails::Action::ActionError,
25
+ unhandled_change_result_action_error_message,
26
+ unhandled_change_result_action_error_backtrace,
27
+ )
28
+ end
29
+ end
30
+ end
31
+
32
+ mixin_class_methods do
33
+ def change_result(&block)
34
+ much_rails_change_action_config.change_result_block = block
35
+ end
36
+ end
37
+
38
+ mixin_instance_methods do
39
+ def change_result
40
+ @change_result ||=
41
+ begin
42
+ unless (
43
+ self.class.much_rails_change_action_config.change_result_block
44
+ )
45
+ raise(Error, undefined_change_result_block_error_message)
46
+ end
47
+
48
+ MuchRails::ChangeActionResult.new(
49
+ instance_exec(
50
+ &self.class.much_rails_change_action_config.change_result_block
51
+ ),
52
+ )
53
+ end
54
+ end
55
+
56
+ # Check the instance variable directly to make sure the main `on_call`
57
+ # block actually called the `change_result` method and memoized a Result.
58
+ # If no Result memoized, there are implicitly no unhandled errors.
59
+ def any_unextracted_change_result_validation_errors?
60
+ !!@change_result&.any_unextracted_validation_errors?
61
+ end
62
+
63
+ private
64
+
65
+ def unhandled_change_result_action_error_message
66
+ change_result.exception&.message ||
67
+ "#{change_result.inspect} has validation errors that were not handled "\
68
+ "by the Action: #{change_result.validation_errors.inspect}."
69
+ end
70
+
71
+ def unhandled_change_result_action_error_backtrace
72
+ change_result.exception&.backtrace || caller
73
+ end
74
+
75
+ def undefined_change_result_block_error_message
76
+ raise NotImplementedError
77
+ end
78
+ end
79
+
80
+ class MuchRailsChangeActionConfig
81
+ attr_accessor :change_result_block
82
+ end
83
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "much-rails/result"
4
+
5
+ module MuchRails; end
6
+
7
+ # MuchRails::ChangeActionResult is a Result object intended to wrap and
8
+ # compose a MuchRails::Result.
9
+ class MuchRails::ChangeActionResult
10
+ def self.success(**kargs)
11
+ new(MuchRails::Result.success(**kargs))
12
+ end
13
+
14
+ def self.failure(**kargs)
15
+ new(MuchRails::Result.failure(**kargs))
16
+ end
17
+
18
+ attr_reader :service_result
19
+
20
+ def initialize(save_service_result)
21
+ unless save_service_result.is_a?(MuchRails::Result)
22
+ raise(
23
+ TypeError,
24
+ "MuchRails::Result expected, got #{save_service_result.class}",
25
+ )
26
+ end
27
+
28
+ @service_result = save_service_result
29
+
30
+ @service_result.validation_errors ||= {}
31
+ end
32
+
33
+ def validation_errors
34
+ @validation_errors ||=
35
+ service_result.get_for_all_results(:validation_errors).to_h
36
+ end
37
+
38
+ def validation_error_messages
39
+ validation_errors.values.flatten.compact
40
+ end
41
+
42
+ def extract_validation_error(field_name)
43
+ validation_errors.delete(field_name)
44
+ end
45
+
46
+ def any_unextracted_validation_errors?
47
+ !!(failure? && validation_errors.any?)
48
+ end
49
+
50
+ private
51
+
52
+ def method_missing(name, *args, &block)
53
+ service_result&.__send__(name, *args, &block)
54
+ end
55
+
56
+ def respond_to_missing?(*args)
57
+ service_result.respond_to?(*args) || super
58
+ end
59
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "much-rails/mixin"
4
+
5
+ module MuchRails; end
6
+
7
+ # MuchRails::Config is a mix-in to implement object DSL configuration.
8
+ module MuchRails::Config
9
+ include MuchRails::Mixin
10
+
11
+ mixin_class_methods do
12
+ def add_config(name = nil, method_name: nil)
13
+ config_method_name, config_class_name, configure_method_name =
14
+ much_rails_config_names(name, method_name)
15
+
16
+ instance_eval(<<~RUBY, __FILE__, __LINE__ + 1)
17
+ def #{config_method_name}
18
+ @#{config_method_name} ||= self::#{config_class_name}.new
19
+ end
20
+
21
+ def #{configure_method_name}
22
+ yield(#{config_method_name}) if block_given?
23
+ end
24
+ RUBY
25
+ end
26
+
27
+ def add_instance_config(name = nil, method_name: nil)
28
+ config_method_name, config_class_name, configure_method_name =
29
+ much_rails_config_names(name, method_name)
30
+
31
+ instance_eval(<<~RUBY, __FILE__, __LINE__ + 1)
32
+ define_method(:#{config_method_name}) do
33
+ @#{config_method_name} ||= self.class::#{config_class_name}.new
34
+ end
35
+
36
+ define_method(:#{configure_method_name}) do |&block|
37
+ block.call(#{config_method_name}) if block
38
+ end
39
+ RUBY
40
+ end
41
+
42
+ private
43
+
44
+ def much_rails_config_names(name, method_name)
45
+ name_prefix = name.nil? ? "" : "#{name.to_s.underscore}_"
46
+ config_method_name = (method_name || "#{name_prefix}config").to_s
47
+ config_class_name = "#{name_prefix.classify}Config"
48
+
49
+ name_suffix = name.nil? ? "" : "_#{name.to_s.underscore}"
50
+ configure_method_name = "configure#{name_suffix}"
51
+
52
+ [config_method_name, config_class_name, configure_method_name]
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MuchRails; end
4
+
5
+ module MuchRails::Date
6
+ InvalidError = Class.new(TypeError)
7
+
8
+ # @example
9
+ # MuchRails::Date.for(nil) # => nil
10
+ # MuchRails::Date.for(" ") # => nil
11
+ # MuchRails::Date.for(Time.zone.today) # => Date
12
+ # MuchRails::Date.for(Time.current) # => Date
13
+ # MuchRails::Date.for(DateTime.current) # => Date
14
+ # MuchRails::Date.for("07/04/2020") # => Date
15
+ # MuchRails::Date.for("2020.07.04") # => Date
16
+ # MuchRails::Date.for("2020-07-04T08:15:00Z") # => Date
17
+ def self.for(value)
18
+ return if value.blank?
19
+
20
+ if value.respond_to?(:to_date) && !value.is_a?(::String)
21
+ value.to_date
22
+ else
23
+ parse(value)
24
+ end
25
+ rescue
26
+ raise MuchRails::Date::InvalidError, "Invalid Date: #{value.inspect}."
27
+ end
28
+
29
+ def self.parse(value)
30
+ parse_united_states(value)
31
+ rescue ArgumentError
32
+ parse8601(value)
33
+ end
34
+
35
+ def self.parse_united_states(value)
36
+ formatted_value = value.to_s.gsub(/[^\w\s:]/, "-")
37
+
38
+ ::Date.strptime(formatted_value, "%m-%d-%Y")
39
+ rescue ArgumentError
40
+ ::Date.strptime(formatted_value, "%Y-%m-%d")
41
+ end
42
+
43
+ def self.parse8601(value)
44
+ formatted_value = value.to_s.gsub(/[^\w\s:]/, "-")
45
+
46
+ ::Date.iso8601(formatted_value)
47
+ rescue ArgumentError
48
+ ::Time.iso8601(formatted_value).utc.to_date
49
+ end
50
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "much-decimal"
4
+
5
+ module MuchRails
6
+ Decimal = MuchDecimal
7
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "much-rails/mixin"
4
+ require "much-rails/change_action"
5
+
6
+ module MuchRails; end
7
+
8
+ module MuchRails::DestroyAction
9
+ include MuchRails::Mixin
10
+
11
+ mixin_included do
12
+ include MuchRails::ChangeAction
13
+ end
14
+
15
+ mixin_class_methods do
16
+ def destroy_result(&block)
17
+ change_result(&block)
18
+ end
19
+ end
20
+
21
+ mixin_instance_methods do
22
+ def destroy_result
23
+ change_result
24
+ end
25
+
26
+ private
27
+
28
+ def undefined_change_result_block_error_message
29
+ "A `destroy_result` block must be defined."
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "much-rails/mixin"
4
+ require "much-rails/records"
5
+ require "much-rails/records/validate_destroy"
6
+ require "much-rails/result"
7
+ require "much-rails/service"
8
+
9
+ module MuchRails; end
10
+
11
+ # MuchRails::DestroyService is a common mix-in for all service objects that
12
+ # destroy records.
13
+ module MuchRails::DestroyService
14
+ include MuchRails::Mixin
15
+
16
+ mixin_included do
17
+ include MuchRails::Service
18
+
19
+ around_call do |receiver|
20
+ receiver.call
21
+ rescue *MuchRails::DestroyService::ValidationErrors.exception_classes => ex
22
+ set_the_return_value_for_the_call_method(
23
+ MuchRails::DestroyService::ValidationErrors.result_for(ex),
24
+ )
25
+ end
26
+ end
27
+
28
+ module ValidationErrors
29
+ def self.add(exception_class, &block)
30
+ service_validation_errors.add(exception_class, &block)
31
+ end
32
+
33
+ def self.exception_classes
34
+ service_validation_errors.exception_classes
35
+ end
36
+
37
+ def self.result_for(ex)
38
+ service_validation_errors.result_for(ex)
39
+ end
40
+
41
+ def self.service_validation_errors
42
+ @service_validation_errors ||=
43
+ MuchRails::ServiceValidationErrors
44
+ .new
45
+ .tap do |e|
46
+ e.add(MuchRails::Records::DestructionInvalid) do |ex|
47
+ MuchRails::DestroyService::FailureResult.new(
48
+ record: ex.record,
49
+ exception: ex,
50
+ validation_errors: ex.errors.to_h,
51
+ validation_error_messages: ex.error_full_messages.to_a,
52
+ )
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ module FailureResult
59
+ def self.new(exception:, validation_errors:, **kargs)
60
+ MuchResult.failure(
61
+ exception: exception,
62
+ validation_errors: validation_errors,
63
+ **kargs,
64
+ )
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "much-slug"
4
+
5
+ module MuchRails
6
+ HasSlug = MuchSlug
7
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MuchRails; end
4
+
5
+ # MuchRails::InputValue is a utility module for dealing with input field values.
6
+ module MuchRails::InputValue
7
+ def self.strip(value)
8
+ return if value.blank?
9
+
10
+ value.to_s.strip
11
+ end
12
+
13
+ def self.strip_all(values)
14
+ Array
15
+ .wrap(values)
16
+ .map{ |value| strip(value) }
17
+ .compact
18
+ end
19
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "oj"
4
+
5
+ module MuchRails; end
6
+
7
+ # MuchRails::JSON is an adapter for encoding and decoding JSON values.
8
+ # It uses Oj to do the work: https://github.com/ohler55/oj#-gem
9
+ module MuchRails::JSON
10
+ InvalidError = Class.new(TypeError)
11
+
12
+ def self.default_mode
13
+ :strict
14
+ end
15
+
16
+ def self.encode(obj, **options)
17
+ options[:mode] ||= default_mode
18
+ ::Oj.dump(obj, options)
19
+ end
20
+
21
+ def self.decode(json, **options)
22
+ options[:mode] ||= default_mode
23
+ ::Oj.load(json, options)
24
+ rescue ::Oj::ParseError => ex
25
+ error = InvalidError.new("Oj::ParseError: #{ex.message}")
26
+ error.set_backtrace(ex.backtrace)
27
+ raise error
28
+ end
29
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "much-rails/layout/helper"
4
+ require "much-rails/mixin"
5
+ require "much-rails/view_models/breadcrumb"
6
+
7
+ module MuchRails; end
8
+
9
+ # MuchRails::Layout is a mix-in for view models that represent HTML rendered
10
+ # in a layout. It adds a DSL for accumulating page titles, stylesheets and
11
+ # javascripts.
12
+ module MuchRails::Layout
13
+ include MuchRails::Mixin
14
+
15
+ mixin_class_methods do
16
+ def page_title(&block)
17
+ page_titles << block
18
+ end
19
+
20
+ def page_titles
21
+ @page_titles ||= []
22
+ end
23
+
24
+ def application_page_title(&block)
25
+ @application_page_title = block if block
26
+ @application_page_title
27
+ end
28
+
29
+ def breadcrumb(&block)
30
+ breadcrumbs << block
31
+ end
32
+
33
+ def breadcrumbs
34
+ @breadcrumbs ||= []
35
+ end
36
+
37
+ def stylesheet(value = nil, &block)
38
+ stylesheets << (block || ->{ value })
39
+ end
40
+
41
+ def stylesheets
42
+ @stylesheets ||= []
43
+ end
44
+
45
+ def javascript(value = nil, &block)
46
+ javascripts << (block || ->{ value })
47
+ end
48
+
49
+ def javascripts
50
+ @javascripts ||= []
51
+ end
52
+
53
+ def head_link(url, **attributes)
54
+ head_links << HeadLink.new(url, **attributes)
55
+ end
56
+
57
+ def head_links
58
+ @head_links ||= []
59
+ end
60
+
61
+ def layout(value)
62
+ layouts << value
63
+ end
64
+
65
+ def layouts
66
+ @layouts ||= []
67
+ end
68
+ end
69
+
70
+ mixin_instance_methods do
71
+ def page_title
72
+ @page_title ||= instance_eval(&(self.class.page_titles.last || ->(_){}))
73
+ end
74
+
75
+ def application_page_title
76
+ @application_page_title ||=
77
+ instance_eval(&(self.class.application_page_title || ->(_){}))
78
+ end
79
+
80
+ def full_page_title
81
+ @full_page_title ||=
82
+ [
83
+ self
84
+ .class
85
+ .page_titles
86
+ .reverse
87
+ .map!{ |segment| instance_eval(&segment) }
88
+ .join(MuchRails.config.layout.full_page_title_segment_separator),
89
+ application_page_title,
90
+ ]
91
+ .map(&:presence)
92
+ .compact
93
+ .join(MuchRails.config.layout.full_page_title_application_separator)
94
+ .presence
95
+ end
96
+
97
+ def breadcrumbs
98
+ @breadcrumbs ||=
99
+ self
100
+ .class
101
+ .breadcrumbs
102
+ .map do |block|
103
+ MuchRails::ViewModels::Breadcrumb.new(*instance_eval(&block))
104
+ end
105
+ end
106
+
107
+ def stylesheets
108
+ @stylesheets ||= self.class.stylesheets.map(&:call)
109
+ end
110
+
111
+ def javascripts
112
+ @javascripts ||= self.class.javascripts.map(&:call)
113
+ end
114
+
115
+ def head_links
116
+ @head_links ||= self.class.head_links
117
+ end
118
+
119
+ def layouts
120
+ self.class.layouts
121
+ end
122
+
123
+ def any_breadcrumbs?
124
+ breadcrumbs.any?
125
+ end
126
+ end
127
+
128
+ class HeadLink
129
+ attr_reader :href, :attributes
130
+
131
+ def initialize(href, **attributes)
132
+ @href = href
133
+ @attributes = attributes
134
+ end
135
+
136
+ def ==(other)
137
+ super unless other.is_a?(self.class)
138
+
139
+ href == other.href && attributes == other.attributes
140
+ end
141
+ end
142
+ end