compony 0.0.1

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 (56) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/.ruby-version +1 -0
  4. data/.yardopts +2 -0
  5. data/Gemfile +3 -0
  6. data/Gemfile.lock +208 -0
  7. data/LICENSE +165 -0
  8. data/README.md +33 -0
  9. data/Rakefile +34 -0
  10. data/app/controllers/compony_controller.rb +31 -0
  11. data/compony.gemspec +32 -0
  12. data/config/locales/de.yml +29 -0
  13. data/config/locales/en.yml +29 -0
  14. data/config/routes.rb +18 -0
  15. data/doc/resourceful_lifecycle.graphml +819 -0
  16. data/doc/resourceful_lifecycle.pdf +1564 -0
  17. data/lib/compony/component.rb +225 -0
  18. data/lib/compony/component_mixins/default/labelling.rb +77 -0
  19. data/lib/compony/component_mixins/default/standalone/resourceful_verb_dsl.rb +55 -0
  20. data/lib/compony/component_mixins/default/standalone/standalone_dsl.rb +56 -0
  21. data/lib/compony/component_mixins/default/standalone/verb_dsl.rb +47 -0
  22. data/lib/compony/component_mixins/default/standalone.rb +117 -0
  23. data/lib/compony/component_mixins/resourceful.rb +92 -0
  24. data/lib/compony/components/button.rb +59 -0
  25. data/lib/compony/components/form.rb +138 -0
  26. data/lib/compony/components/resourceful/destroy.rb +77 -0
  27. data/lib/compony/components/resourceful/edit.rb +96 -0
  28. data/lib/compony/components/resourceful/new.rb +95 -0
  29. data/lib/compony/components/with_form.rb +37 -0
  30. data/lib/compony/controller_mixin.rb +12 -0
  31. data/lib/compony/engine.rb +19 -0
  32. data/lib/compony/method_accessible_hash.rb +43 -0
  33. data/lib/compony/model_fields/anchormodel.rb +28 -0
  34. data/lib/compony/model_fields/association.rb +53 -0
  35. data/lib/compony/model_fields/base.rb +63 -0
  36. data/lib/compony/model_fields/boolean.rb +9 -0
  37. data/lib/compony/model_fields/currency.rb +9 -0
  38. data/lib/compony/model_fields/date.rb +9 -0
  39. data/lib/compony/model_fields/datetime.rb +9 -0
  40. data/lib/compony/model_fields/decimal.rb +6 -0
  41. data/lib/compony/model_fields/float.rb +6 -0
  42. data/lib/compony/model_fields/integer.rb +6 -0
  43. data/lib/compony/model_fields/phone.rb +15 -0
  44. data/lib/compony/model_fields/rich_text.rb +9 -0
  45. data/lib/compony/model_fields/string.rb +6 -0
  46. data/lib/compony/model_fields/text.rb +6 -0
  47. data/lib/compony/model_fields/time.rb +6 -0
  48. data/lib/compony/model_mixin.rb +88 -0
  49. data/lib/compony/request_context.rb +45 -0
  50. data/lib/compony/version.rb +11 -0
  51. data/lib/compony/view_helpers.rb +36 -0
  52. data/lib/compony.rb +268 -0
  53. data/lib/generators/component/USAGE +8 -0
  54. data/lib/generators/component/component_generator.rb +14 -0
  55. data/lib/generators/component/templates/component.rb.erb +4 -0
  56. metadata +236 -0
@@ -0,0 +1,59 @@
1
+ module Compony
2
+ module Components
3
+ # @api description
4
+ # This is the default button implementation, providing a minimal button
5
+ class Button < Compony::Component
6
+ SUPPORTED_TYPES = %i[button submit].freeze
7
+
8
+ # path: If given a block, it will be evaluated in the helpers context when rendering
9
+ # enabled: If given a block, it will be evaluated in the helpers context when rendering
10
+ def initialize(*args, label: nil, path: nil, method: nil, type: nil, enabled: nil, visible: nil, title: nil, **kwargs, &block)
11
+ @label = label || Compony.button_defaults[:label]
12
+ @type = type&.to_sym || Compony.button_defaults[:type] || :button
13
+ @path = path || Compony.button_defaults[:path] || 'javascript:void(0)'
14
+ @method = method || Compony.button_defaults[:method]
15
+ if @type != :button && !@method.nil?
16
+ fail("Param `method` is only allowed for :button type buttons, but got method #{@method.inspect} for type #{@type.inspect}")
17
+ end
18
+ @method ||= :get
19
+ @enabled = enabled
20
+ @enabled = Compony.button_defaults[:enabled] if @enabled.nil?
21
+ @enabled = true if @enabled.nil?
22
+ @visible = visible
23
+ @visible = Compony.button_defaults[:visible] if @visible.nil?
24
+ @visible = true if @visible.nil?
25
+ @title = title || Compony.button_defaults[:title]
26
+
27
+ fail "Unsupported button type #{@type}, use on of: #{SUPPORTED_TYPES.inspect}" unless SUPPORTED_TYPES.include?(@type)
28
+
29
+ super(*args, **kwargs, &block)
30
+ end
31
+
32
+ setup do
33
+ before_render do
34
+ if @path.respond_to?(:call)
35
+ @path = instance_exec(&@path)
36
+ end
37
+ if @enabled.respond_to?(:call)
38
+ @enabled = @enabled.call(controller)
39
+ end
40
+ if @visible.respond_to?(:call)
41
+ @visible = @visible.call(controller)
42
+ end
43
+ @path = 'javascript:void(0)' unless @enabled
44
+ end
45
+
46
+ content do
47
+ if @visible
48
+ case @type
49
+ when :button
50
+ concat button_to(@label, @path, method: @method, disabled: !@enabled, title: @title)
51
+ when :submit
52
+ concat button_tag(@label, type: :submit, disabled: !@enabled, title: @title)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,138 @@
1
+ module Compony
2
+ module Components
3
+ # @api description
4
+ # This component is used for the _form partial in the Rails paradigm.
5
+ class Form < Component
6
+ def initialize(...)
7
+ @schema_lines_for_data = [] # Array of procs taking data returning a Schemacop proc
8
+ super
9
+ end
10
+
11
+ def check_config!
12
+ super
13
+ fail "#{inspect} requires config.form_fields do ..." if @form_fields.blank?
14
+ end
15
+
16
+ setup do
17
+ before_render do
18
+ # Must render the buttons now as the rendering within simple form breaks the form
19
+ @submit_button = Compony.button_component_class.new(
20
+ label: @submit_label || I18n.t('compony.components.form.submit'), icon: 'arrow-right', type: :submit
21
+ ).render(controller)
22
+ @submit_path = @comp_opts[:submit_path]
23
+ @submit_path = @submit_path.call(controller) if @submit_path.respond_to?(:call)
24
+ end
25
+
26
+ content do
27
+ form_html = simple_form_for(data, method: @comp_opts[:submit_verb], url: @submit_path) do |f|
28
+ component.with_simpleform(f) do
29
+ instance_exec(&form_fields)
30
+ div @submit_button, class: 'compony-form-buttons'
31
+ end
32
+ end
33
+ concat form_html
34
+ end
35
+ end
36
+
37
+ # DSL method, use to set the form content
38
+ def form_fields(&block)
39
+ return @form_fields unless block_given?
40
+ @form_fields = block
41
+ end
42
+
43
+ # Attr reader for @schema_wrapper_key with auto-calculated default
44
+ def schema_wrapper_key_for(data)
45
+ if @schema_wrapper_key.present?
46
+ return @schema_wrapper_key
47
+ else
48
+ # If schema was not called, auto-infer a default
49
+ data.model_name.singular
50
+ end
51
+ end
52
+
53
+ # Attr reader for @schema_block with auto-calculated default
54
+ def schema_block_for(data)
55
+ if @schema_block
56
+ return @schema_block
57
+ else
58
+ # If schema was not called, auto-infer a default
59
+ local_schema_lines_for_data = @schema_lines_for_data
60
+ return proc do
61
+ local_schema_lines_for_data.each do |schema_line|
62
+ instance_exec(&schema_line.call(data))
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ # This method is used by render to store the simpleform instance inside the component such that we can call
69
+ # methods from inside `form_fields`. This is a workaround required because the form does not exist when the
70
+ # RequestContext is being built, and we want the method `field` to be available inside the `form_fields` block.
71
+ # @todo Refactor? Could this be greatly simplified by having `form_field to |f|` ?
72
+ def with_simpleform(simpleform)
73
+ @simpleform = simpleform
74
+ yield
75
+ @simpleform = nil
76
+ end
77
+
78
+ # Called inside the form_fields block. This makes the method `field` available in the block.
79
+ # See also notes for `with_simpleform`.
80
+ def field(name, **input_opts)
81
+ fail("The `field` method may only be called inside `form_fields` for #{inspect}.") unless @simpleform
82
+
83
+ hidden = input_opts.delete(:hidden)
84
+ model_field = @simpleform.object.fields[name.to_sym]
85
+ fail("Field #{name.to_sym.inspect} is not defined on #{@simpleform.object.inspect} but was requested in #{inspect}.") unless model_field
86
+
87
+ if hidden
88
+ return @simpleform.input model_field.schema_key, as: :hidden, **input_opts
89
+ else
90
+ return model_field.simpleform_input(@simpleform, self, **input_opts)
91
+ end
92
+ end
93
+
94
+ # Called inside the form_fields block. This makes the method `f` available in the block.
95
+ # See also notes for `with_simpleform`.
96
+ def f
97
+ fail("The `f` method may only be called inside `form_fields` for #{inspect}.") unless @simpleform
98
+ return @simpleform
99
+ end
100
+
101
+ # Quick access for wrapping collections in Rails compatible format
102
+ def collect(...)
103
+ Compony::ModelFields::Anchormodel.collect(...)
104
+ end
105
+
106
+ protected
107
+
108
+ # DSL method, adds a new line to the schema whitelisting a single param inside the schema's wrapper
109
+ def schema_line(&block)
110
+ @schema_lines_for_data << proc { block }
111
+ end
112
+
113
+ # DSL method, adds a new field to the schema whitelisting a single field of data_class
114
+ # This auto-generates the correct schema line for the field.
115
+ def schema_field(field_name)
116
+ @schema_lines_for_data << proc do |data|
117
+ field = data.class.fields[field_name.to_sym] || fail("No field #{field_name.to_sym.inspect} found for #{data.inspect} in #{inspect}.")
118
+ next field.schema_line
119
+ end
120
+ end
121
+
122
+ # DSL method, mass-assigns schema fields
123
+ def schema_fields(*field_names)
124
+ field_names.each { |field_name| schema_field(field_name) }
125
+ end
126
+
127
+ # DSL method, use to replace the form's schema and wrapper key for a completely manual schema
128
+ def schema(wrapper_key, &block)
129
+ if block_given?
130
+ @schema_wrapper_key = wrapper_key
131
+ @schema_block = block
132
+ else
133
+ fail 'schema requires a block to be given'
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,77 @@
1
+ module Compony
2
+ module Components
3
+ module Resourceful
4
+ # @api description
5
+ # This component is used for the Rails destroy paradigm. Asks for confirm when queried using GET.
6
+ class Destroy < Compony::Component
7
+ include Compony::ComponentMixins::Resourceful
8
+
9
+ setup do
10
+ standalone path: "#{family_name}/:id/destroy" do
11
+ verb :get do
12
+ authorize { can?(:destroy, @data) }
13
+ end
14
+
15
+ verb :delete do
16
+ authorize { can?(:destroy, @data) }
17
+ store_data # This enables the global store_data block defined below for this path and verb.
18
+ respond do
19
+ evaluate_with_backfire(&@on_destroyed_block)
20
+ end
21
+ end
22
+ end
23
+
24
+ label(:long) { |data| I18n.t('compony.components.destroy.label.long', data_label: data.label) }
25
+ label(:short) { |_| I18n.t('compony.components.destroy.label.short') }
26
+ icon { :trash }
27
+ color { :danger }
28
+
29
+ content do
30
+ div I18n.t('compony.components.destroy.confirm_question', data_label: @data.label)
31
+ div do
32
+ concat compony_button(comp_cst,
33
+ @data,
34
+ label: I18n.t('compony.components.destroy.confirm_button'),
35
+ method: :delete)
36
+ end
37
+ end
38
+
39
+ store_data do
40
+ # Validate params against the form's schema
41
+ local_data = @data # Capture data for usage in the Schemacop call
42
+ schema = Schemacop::Schema3.new :hash, additional_properties: true do
43
+ if local_data.class.primary_key_type_key == :string
44
+ str! :id
45
+ else
46
+ int! :id, cast_str: true
47
+ end
48
+ end
49
+ schema.validate!(controller.request.params)
50
+
51
+ # Perform destroy
52
+ @data.destroy!
53
+ end
54
+
55
+ on_destroyed do
56
+ flash.notice = I18n.t('compony.components.destroy.data_was_destroyed', data_label: @data.label)
57
+ redirect_to evaluate_with_backfire(&@on_destroyed_redirect_path_block), status: :see_other # 303: force GET
58
+ end
59
+
60
+ on_destroyed_redirect_path do
61
+ Compony.path(:index, family_cst)
62
+ end
63
+ end
64
+
65
+ # DSL method
66
+ def on_destroyed(&block)
67
+ @on_destroyed_block = block
68
+ end
69
+
70
+ # DSL method
71
+ def on_destroyed_redirect_path(&block)
72
+ @on_destroyed_redirect_path_block = block
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,96 @@
1
+ module Compony
2
+ module Components
3
+ module Resourceful
4
+ # @api description
5
+ # This component is used for the Rails edit and update paradigm. Performs update when the form is submitted.
6
+ class Edit < Compony::Components::WithForm
7
+ include Compony::ComponentMixins::Resourceful
8
+ setup do
9
+ submit_verb :patch
10
+ standalone path: "#{family_name}/:id/edit" do
11
+ verb :get do
12
+ authorize { can?(:edit, @data) }
13
+ assign_attributes # This enables the global assign_attributes block defined below for this path and verb.
14
+ end
15
+ verb submit_verb do
16
+ authorize { can?(:update, @data) }
17
+ assign_attributes # This enables the global assign_attributes block defined below for this path and verb.
18
+ store_data # This enables the global store_data block defined below for this path and verb.
19
+ respond do
20
+ if @update_succeeded
21
+ evaluate_with_backfire(&@on_updated_block)
22
+ else
23
+ evaluate_with_backfire(&@on_update_failed_block)
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ label(:long) { |data| I18n.t('compony.components.edit.label.long', data_label: data.label) }
30
+ label(:short) { |_| I18n.t('compony.components.edit.label.short') }
31
+ icon { :pencil }
32
+
33
+ content do
34
+ concat form_comp.render(controller, data: @data)
35
+ end
36
+
37
+ assign_attributes do
38
+ # Validate params against the form's schema
39
+ local_form_comp = form_comp # Capture form_comp for usage in the Schemacop call
40
+ local_data = @data # Capture data for usage in the Schemacop call
41
+ schema = Schemacop::Schema3.new :hash, additional_properties: true do
42
+ if local_data.class.primary_key_type_key == :string
43
+ str! :id
44
+ else
45
+ int! :id, cast_str: true
46
+ end
47
+ hsh? local_form_comp.schema_wrapper_key_for(local_data), &local_form_comp.schema_block_for(local_data)
48
+ end
49
+ schema.validate!(controller.request.params)
50
+
51
+ # TODO: Why are we not saving the validated params?
52
+ attrs_to_assign = controller.request.params[form_comp.schema_wrapper_key_for(@data)]
53
+ @data.assign_attributes(attrs_to_assign) if attrs_to_assign
54
+ end
55
+
56
+ store_data do
57
+ @update_succeeded = @data.save
58
+ end
59
+
60
+ on_updated do
61
+ flash.notice = I18n.t('compony.components.edit.data_was_updated', data_label: data.label)
62
+ redirect_to evaluate_with_backfire(&@on_updated_redirect_path_block)
63
+ end
64
+
65
+ on_updated_redirect_path do
66
+ if Compony.comp_class_for(:show, @data)
67
+ Compony.path(:show, @data)
68
+ else
69
+ Compony.path(:index, @data)
70
+ end
71
+ end
72
+
73
+ on_update_failed do
74
+ Rails.logger.warn(@data&.errors&.full_messages)
75
+ render_standalone(controller, status: :unprocessable_entity)
76
+ end
77
+ end
78
+
79
+ # DSL method
80
+ def on_updated(&block)
81
+ @on_updated_block = block
82
+ end
83
+
84
+ # DSL method
85
+ def on_updated_redirect_path(&block)
86
+ @on_updated_redirect_path_block = block
87
+ end
88
+
89
+ # DSL method
90
+ def on_update_failed(&block)
91
+ @on_update_failed_block = block
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,95 @@
1
+ module Compony
2
+ module Components
3
+ module Resourceful
4
+ # @api description
5
+ # This component is used for the Rails new and create paradigm. Performs update when the form is submitted.
6
+ class New < Compony::Components::WithForm
7
+ include Compony::ComponentMixins::Resourceful
8
+
9
+ setup do
10
+ submit_verb :post
11
+ load_data { @data = data_class.new }
12
+ standalone path: "#{family_name}/new" do
13
+ verb :get do
14
+ authorize { can?(:create, data_class) }
15
+ assign_attributes # This enables the global assign_attributes block defined below for this path and verb.
16
+ end
17
+ verb submit_verb do
18
+ authorize { can?(:create, data_class) }
19
+ assign_attributes # This enables the global assign_attributes block defined below for this path and verb.
20
+ store_data # This enables the global store_data block defined below for this path and verb.
21
+ respond do
22
+ if @create_succeeded
23
+ evaluate_with_backfire(&@on_created_block)
24
+ else
25
+ evaluate_with_backfire(&@on_create_failed_block)
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ label(:long) { I18n.t('compony.components.new.label.long', data_class: data_class.model_name.human) }
32
+ label(:short) { I18n.t('compony.components.new.label.short') }
33
+ icon { :plus }
34
+
35
+ add_content do
36
+ h2 component.label
37
+ end
38
+ add_content do
39
+ concat form_comp.render(controller, data: @data)
40
+ end
41
+
42
+ assign_attributes do
43
+ local_form_comp = form_comp # Capture form_comp for usage in the Schemacop call
44
+ local_data = @data # Capture data for usage in the Schemacop call
45
+ schema = Schemacop::Schema3.new :hash, additional_properties: true do
46
+ hsh? local_form_comp.schema_wrapper_key_for(local_data), &local_form_comp.schema_block_for(local_data)
47
+ end
48
+ schema.validate!(controller.request.params)
49
+
50
+ # TODO: Why are we not saving the validated params?
51
+ attrs_to_assign = controller.request.params[form_comp.schema_wrapper_key_for(@data)]
52
+ @data.assign_attributes(attrs_to_assign) if attrs_to_assign
53
+ end
54
+
55
+ store_data do
56
+ @create_succeeded = @data.save
57
+ end
58
+
59
+ on_created do
60
+ flash.notice = I18n.t('compony.components.new.data_was_created', data_label: data.label)
61
+ redirect_to evaluate_with_backfire(&@on_created_redirect_path_block)
62
+ end
63
+
64
+ on_created_redirect_path do
65
+ if Compony.comp_class_for(:show, @data)
66
+ Compony.path(:show, @data)
67
+ else
68
+ Compony.path(:index, @data)
69
+ end
70
+ end
71
+
72
+ on_create_failed do
73
+ Rails.logger.warn(@data&.errors&.full_messages)
74
+ render_standalone(controller, status: :unprocessable_entity)
75
+ end
76
+ end
77
+
78
+ # DSL method
79
+ def on_created(&block)
80
+ @on_created_block = block
81
+ end
82
+
83
+ # DSL method
84
+ def on_created_redirect_path(&block)
85
+ @on_created_redirect_path_block = block
86
+ end
87
+
88
+ # DSL method
89
+ def on_create_failed(&block)
90
+ @on_create_failed_block = block
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,37 @@
1
+ module Compony
2
+ module Components
3
+ # @api description
4
+ # This component is destined to take a sub-component that is a form component.
5
+ # It can be called via :get or via `submit_verb` depending on whether its form should be shown or submitted.
6
+ class WithForm < Component
7
+ # Returns an instance of the form component responsible for rendering the form.
8
+ # Feel free to override this in subclasses.
9
+ def form_comp
10
+ @form_comp ||= (form_comp_class || comp_class_for!(:form, family_cst)).new(
11
+ self,
12
+ submit_verb:,
13
+ # If applicable, Rails adds the route keys automatically, thus, e.g. :id does not need to be passed here, as it comes from the request.
14
+ submit_path: ->(controller) { controller.helpers.send("#{Compony.path_helper_name(comp_name, family_name)}_path") }
15
+ )
16
+ end
17
+
18
+ # DSL method
19
+ # Sets or returns the previously set submit verb
20
+ def submit_verb(new_submit_verb = nil)
21
+ if new_submit_verb.present?
22
+ new_submit_verb = new_submit_verb.to_sym
23
+ available_verbs = ComponentMixins::Default::Standalone::VerbDsl::AVAILABLE_VERBS
24
+ fail "Unknown HTTP verb #{new_submit_verb.inspect}, use one of #{available_verbs.inspect}" unless available_verbs.include?(new_submit_verb)
25
+ @submit_verb = new_submit_verb
26
+ end
27
+ return @submit_verb || fail("WithForm component #{self} is missing a call to `submit_verb`.")
28
+ end
29
+
30
+ # DSL method
31
+ # Overrides the form comp class that is instanciated to render the form
32
+ def form_comp_class(new_form_comp_class = nil)
33
+ @form_comp_class ||= new_form_comp_class
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,12 @@
1
+ module Compony
2
+ module ControllerMixin
3
+ extend ActiveSupport::Concern
4
+
5
+ include Compony::ViewHelpers
6
+
7
+ included do
8
+ # Declare all methods in each such module as helper_method
9
+ Compony::ViewHelpers.public_instance_methods.each { |helper_method_sym| helper_method helper_method_sym }
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,19 @@
1
+ module Compony
2
+ class Engine < Rails::Engine
3
+ initializer 'compony.configure_eager_load_paths', before: :load_environment_hook, group: :all do
4
+ # Allow app/components/foo/bar.rb to define constants Components::Foo::Bar and make sure components are eager loaded (needed for route generation etc.)
5
+ Rails.application.config.eager_load_paths.delete(Rails.root.join('app', 'components').to_s)
6
+ Rails.application.config.eager_load_paths.unshift(Rails.root.join('app').to_s)
7
+
8
+ # Prevent *.rb files in assets and views directories to be loaded
9
+ Rails.autoloaders.main.ignore(Rails.root.join('app', 'assets').to_s)
10
+ Rails.autoloaders.main.ignore(Rails.root.join('app', 'views').to_s)
11
+ end
12
+
13
+ initializer 'compony.controller_mixin' do
14
+ ActiveSupport.on_load :action_controller_base do
15
+ include Compony::ControllerMixin
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,43 @@
1
+ module Compony
2
+ # @api description
3
+ # This class is intended for configs with predefined interfaces and should be used with instances of Hash:<br>
4
+ # Example:<br>
5
+ # ```ruby
6
+ # instance_of_a_hash = Compony::MethodAccessibleHash.new
7
+ # instance_of_a_hash.merge!({ foo: :bar })
8
+ # instance_of_a_hash.foo --> :bar
9
+ # instance_of_a_hash.roo --> nil
10
+ # ```
11
+ # See: https://gist.github.com/kalsan/87826048ea0ade92ab1be93c0919b405
12
+ class MethodAccessibleHash < Hash
13
+ # Takes an optional hash as argument and constructs a new
14
+ # MethodAccessibleHash.
15
+ def initialize(hash = {})
16
+ super()
17
+
18
+ hash.each do |key, value|
19
+ self[key.to_sym] = value
20
+ end
21
+ end
22
+
23
+ # @private
24
+ def merge(hash)
25
+ super(hash.symbolize_keys)
26
+ end
27
+
28
+ # @private
29
+ def method_missing(method, *args, &)
30
+ if method.end_with?('=')
31
+ name = method.to_s.gsub(/=$/, '')
32
+ self[name.to_sym] = args.first
33
+ else
34
+ self[method.to_sym]
35
+ end
36
+ end
37
+
38
+ # @private
39
+ def respond_to_missing?(_method, _include_private = false)
40
+ true
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,28 @@
1
+ module Compony
2
+ module ModelFields
3
+ class Anchormodel < Base
4
+ # Takes an array of objects implementing the methods `label` and `key` and returns an array suitable for simple_form select fields.
5
+ def self.collect(flat_array, label_method: :label, key_method: :key)
6
+ return flat_array.map { |entry| [entry.send(label_method), entry.send(key_method)] }
7
+ end
8
+
9
+ def value_for(data, controller: nil, **_)
10
+ return transform_and_join(data.send(@name), controller:, &:label)
11
+ end
12
+
13
+ def simpleform_input(form, _component, **input_opts)
14
+ selected_cst = form.object.send(@name)
15
+ anchormodel_attribute = @model_class.anchormodel_attributes[@name]
16
+ anchormodel_class = anchormodel_attribute.anchormodel_class
17
+ opts = {
18
+ collection: self.class.collect(anchormodel_class.all),
19
+ label_method: :first,
20
+ value_method: :second,
21
+ selected: selected_cst&.key || anchormodel_class.all.first,
22
+ include_blank: anchormodel_attribute.optional
23
+ }.merge(input_opts)
24
+ return form.input @name, **opts
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,53 @@
1
+ module Compony
2
+ module ModelFields
3
+ class Association < Base
4
+ def initialize(...)
5
+ super
6
+ resolve_association!
7
+ end
8
+
9
+ def value_for(data, link_to_component: nil, link_opts: {}, controller: nil)
10
+ if link_to_component
11
+ return transform_and_join(data.send(@name), controller:) do |el|
12
+ el.nil? ? nil : controller.helpers.compony_link(link_to_component, el, **link_opts)
13
+ end
14
+ else
15
+ return transform_and_join(data.send(@name), controller:) { |el| el&.label }
16
+ end
17
+ end
18
+
19
+ def schema_line
20
+ local_schema_key = @schema_key # Capture schema_key as it will not be available within the lambda
21
+ if multi?
22
+ return proc do
23
+ ary? local_schema_key do
24
+ list :integer, cast_str: true
25
+ end
26
+ end
27
+ else
28
+ return proc do
29
+ int? local_schema_key, cast_str: true
30
+ end
31
+ end
32
+ end
33
+
34
+ def simpleform_input(form, _component, **input_opts)
35
+ return form.association @name, **input_opts
36
+ end
37
+
38
+ protected
39
+
40
+ # Uses Rails methods to figure out the arity, schema key etc. and store them.
41
+ # This can be auto-inferred without accessing the database.
42
+ def resolve_association!
43
+ @association = true
44
+ association_info = @model_class.reflect_on_association(@name) || fail("Association #{@name.inspect} does not exist for #{@model_class.inspect}.")
45
+ @multi = association_info.macro == :has_many
46
+ id_name = "#{@name.to_s.singularize}_id"
47
+ @schema_key = @multi ? id_name.pluralize.to_sym : id_name.to_sym
48
+ rescue ActiveRecord::NoDatabaseError
49
+ Rails.logger.warn('Warning: Compony could not auto-detect fields due to missing database. This is ok when running db:create.')
50
+ end
51
+ end
52
+ end
53
+ end