view_component_reducible 0.1.1 → 0.1.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 63ab84272ee25669a3647287a779462deb0ea3ae4b12327cc7b5b632a3bcfb08
4
- data.tar.gz: 57ce9c56c9aa55d61597f22faab956ffcc9865f93e9296ba08f6c0881cf84fc1
3
+ metadata.gz: a699ce3503948db085a77d5d187e725157966bf53a132e9d9124a772db648f1c
4
+ data.tar.gz: ba68826392cd3ea83ca6ceea8f8f753d893f013467d61eff20f3085fd40c9e0c
5
5
  SHA512:
6
- metadata.gz: bd8662871ea30fd81a8ffc695067322fc7ff23fdaf625cd1e43bedab36bc198962aa7210d320ec79532fb88f784b03492ab23112b3fdfbc9a3f906cc7762e9a0
7
- data.tar.gz: e8fdda9358af6377de808a9beaa016d9e3603c4b167978c7230e882f7a551e27e2840d2239bb777c1e6ef56299f443a8e6e6af9374930da8683c6269b18e63b6
6
+ metadata.gz: 9eca692a8a5b7185869237597cb79d199d4e1e036501f2c9c2574e40ea643e99535202f31fb44198a51e293e2fd5078adefd2e65756bd95fc8732917a688b892
7
+ data.tar.gz: bfe0b991478a5a8788f2b2f9c69f7705d5ad9bf1de828d76d50c5b652a8d57bb23c18dfe883289bf231c54b77885e311c35485e9d4a7fc37f200b87d113033a8
data/README.md CHANGED
@@ -9,7 +9,7 @@ The only thing you add in Rails is a single endpoint:
9
9
  mount ViewComponentReducible::Engine, at: "/vcr"
10
10
  ```
11
11
 
12
- Everything else stays inside ViewComponent—no extra endpoints, controllers, WebSockets, or JS frameworks required.
12
+ Everything else is **reducible** to ViewComponent—no extra endpoints, controllers, WebSockets, or JS frameworks required.
13
13
 
14
14
  view_component_reducible brings reducer-based state transitions
15
- to Rails ViewComponent, inspired by TEA (The Elm Architecture).
15
+ to Rails ViewComponent, inspired by TEA (The Elm Architecture).
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support"
4
- require "active_support/message_verifier"
3
+ require 'active_support'
4
+ require 'active_support/message_verifier'
5
5
 
6
6
  module ViewComponentReducible
7
7
  module Adapter
@@ -9,20 +9,21 @@ module ViewComponentReducible
9
9
  class HiddenField < Base
10
10
  # @return [ActiveSupport::MessageVerifier]
11
11
  def verifier
12
- @verifier ||= ActiveSupport::MessageVerifier.new(@secret, digest: "SHA256", serializer: JSON)
12
+ @verifier ||= ActiveSupport::MessageVerifier.new(@secret, digest: 'SHA256', serializer: JSON)
13
13
  end
14
14
 
15
15
  # @param envelope [Hash]
16
16
  # @param request [ActionDispatch::Request]
17
17
  # @return [String]
18
- def dump(envelope, request:)
18
+ def dump(envelope, request: nil)
19
+ _ = request
19
20
  verifier.generate(envelope)
20
21
  end
21
22
 
22
23
  # @param request [ActionDispatch::Request]
23
24
  # @return [Hash]
24
25
  def load(request:)
25
- signed = request.params.fetch("vcr_state")
26
+ signed = request.params.fetch('vcr_state')
26
27
  verifier.verify(signed)
27
28
  end
28
29
  end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support"
4
- require "active_support/message_verifier"
5
- require "securerandom"
3
+ require 'active_support'
4
+ require 'active_support/message_verifier'
5
+ require 'securerandom'
6
6
 
7
7
  module ViewComponentReducible
8
8
  module Adapter
@@ -10,7 +10,7 @@ module ViewComponentReducible
10
10
  class Session < Base
11
11
  # @return [ActiveSupport::MessageVerifier]
12
12
  def verifier
13
- @verifier ||= ActiveSupport::MessageVerifier.new(@secret, digest: "SHA256", serializer: JSON)
13
+ @verifier ||= ActiveSupport::MessageVerifier.new(@secret, digest: 'SHA256', serializer: JSON)
14
14
  end
15
15
 
16
16
  # @param envelope [Hash]
@@ -19,15 +19,15 @@ module ViewComponentReducible
19
19
  def dump(envelope, request:)
20
20
  key = SecureRandom.hex(16)
21
21
  request.session["vcr:#{key}"] = envelope
22
- verifier.generate({ "k" => key })
22
+ verifier.generate({ 'k' => key })
23
23
  end
24
24
 
25
25
  # @param request [ActionDispatch::Request]
26
26
  # @return [Hash]
27
27
  def load(request:)
28
- signed = request.params.fetch("vcr_state")
28
+ signed = request.params.fetch('vcr_state')
29
29
  payload = verifier.verify(signed)
30
- key = payload.fetch("k")
30
+ key = payload.fetch('k')
31
31
  request.session.fetch("vcr:#{key}")
32
32
  end
33
33
  end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'nokogiri'
4
+ require 'securerandom'
5
+
3
6
  module ViewComponentReducible
4
7
  # Include to enable the component state DSL and helpers.
5
8
  module Component
@@ -27,13 +30,12 @@ module ViewComponentReducible
27
30
  end
28
31
 
29
32
  # Build state hash for rendering from the envelope.
30
- # @return [Hash{String=>Hash}]
33
+ # @return [Hash{String=>Object}]
31
34
  def vcr_state
32
- return { "data" => {}, "meta" => {} } if vcr_envelope.nil?
35
+ return {} if vcr_envelope.nil?
33
36
 
34
37
  schema = self.class.vcr_state_schema
35
- data, meta = schema.build(vcr_envelope["data"], vcr_envelope["meta"])
36
- { "data" => data, "meta" => meta }
38
+ schema.build(vcr_envelope['data'])
37
39
  end
38
40
 
39
41
  # Optional DOM target id for updates.
@@ -43,6 +45,24 @@ module ViewComponentReducible
43
45
  "vcr:#{self.class.vcr_id}:#{path}"
44
46
  end
45
47
 
48
+ # Render component markup wrapped in a VCR boundary when available.
49
+ # @param view_context [ActionView::Base]
50
+ # @return [String]
51
+ def render_in(view_context, &block)
52
+ ensure_vcr_state(view_context) if vcr_envelope.nil?
53
+
54
+ path = vcr_envelope && vcr_envelope['path']
55
+ previous_path = view_context.instance_variable_get(:@vcr_current_path)
56
+ view_context.instance_variable_set(:@vcr_current_path, path)
57
+
58
+ rendered = super
59
+ return rendered if path.nil? || path.to_s.empty?
60
+
61
+ inject_vcr_path(rendered, path)
62
+ ensure
63
+ view_context.instance_variable_set(:@vcr_current_path, previous_path)
64
+ end
65
+
46
66
  module ClassMethods
47
67
  # Stable component identifier for envelopes.
48
68
  # @return [String]
@@ -50,5 +70,33 @@ module ViewComponentReducible
50
70
  name.to_s
51
71
  end
52
72
  end
73
+
74
+ private
75
+
76
+ def inject_vcr_path(rendered, path)
77
+ fragment = Nokogiri::HTML::DocumentFragment.parse(rendered.to_s)
78
+ root = fragment.children.find(&:element?)
79
+ return rendered if root.nil?
80
+
81
+ root['data-vcr-path'] = path
82
+ html = fragment.to_html
83
+ html.respond_to?(:html_safe) ? html.html_safe : html
84
+ end
85
+
86
+ def ensure_vcr_state(view_context)
87
+ return unless view_context.respond_to?(:controller)
88
+
89
+ controller = view_context.controller
90
+ return unless controller.respond_to?(:request)
91
+
92
+ envelope = State::Envelope.initial(self.class, path: next_root_path(view_context))
93
+ adapter = ViewComponentReducible.config.adapter_for(controller)
94
+ @vcr_state_token = adapter.dump(envelope, request: controller.request)
95
+ @vcr_envelope = envelope
96
+ end
97
+
98
+ def next_root_path(_view_context)
99
+ "root/#{SecureRandom.uuid}"
100
+ end
53
101
  end
54
102
  end
@@ -15,9 +15,9 @@ module ViewComponentReducible
15
15
  # @param controller [ActionController::Base]
16
16
  # @param adapter_class [Class, nil]
17
17
  # @return [ViewComponentReducible::Adapter::Base]
18
- def adapter_for(controller, adapter_class: nil)
18
+ def adapter_for(_controller, adapter_class: nil)
19
19
  resolved_secret = secret || default_secret
20
- raise "ViewComponentReducible secret is missing" if resolved_secret.nil?
20
+ raise 'ViewComponentReducible secret is missing' if resolved_secret.nil?
21
21
 
22
22
  (adapter_class || adapter).new(secret: resolved_secret)
23
23
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "erb"
3
+ require 'erb'
4
4
 
5
5
  module ViewComponentReducible
6
6
  # Helpers for dispatch responses.
@@ -24,7 +24,7 @@ module ViewComponentReducible
24
24
  SCRIPT
25
25
 
26
26
  injection = meta + script
27
- html.include?("</head>") ? html.sub("</head>", "#{injection}</head>") : (injection + html)
27
+ html.include?('</head>') ? html.sub('</head>', "#{injection}</head>") : (injection + html)
28
28
  end
29
29
  end
30
30
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "action_controller"
3
+ require 'action_controller'
4
4
 
5
5
  module ViewComponentReducible
6
6
  # Rails controller entry for dispatch requests.
@@ -21,7 +21,7 @@ module ViewComponentReducible
21
21
  adapter = ViewComponentReducible.config.adapter_for(self, adapter_class: adapter_class)
22
22
  envelope = adapter.load(request:)
23
23
  msg = ViewComponentReducible::Msg.from_params(params)
24
- target_path = params.fetch("vcr_target_path", envelope["path"])
24
+ target_path = params.fetch('vcr_target_path', envelope['path'])
25
25
 
26
26
  runtime = ViewComponentReducible::Runtime.new
27
27
  new_envelope, html = runtime.call(
@@ -32,15 +32,15 @@ module ViewComponentReducible
32
32
  )
33
33
 
34
34
  signed = adapter.dump(new_envelope, request:)
35
- if params["vcr_partial"] == "1"
35
+ if params['vcr_partial'] == '1'
36
36
  partial_html = runtime.render_target(envelope: new_envelope, target_path:, controller: self)
37
- response.set_header("X-VCR-State", signed)
38
- render html: partial_html, content_type: "text/html"
37
+ response.set_header('X-VCR-State', signed)
38
+ render html: partial_html, content_type: 'text/html'
39
39
  else
40
- render html: ViewComponentReducible::Dispatch.inject_state(html, signed), content_type: "text/html"
40
+ render html: ViewComponentReducible::Dispatch.inject_state(html, signed), content_type: 'text/html'
41
41
  end
42
42
  rescue ActiveSupport::MessageVerifier::InvalidSignature
43
- render status: 400, plain: "Invalid state signature"
43
+ render status: 400, plain: 'Invalid state signature'
44
44
  end
45
45
  end
46
46
  end
@@ -1,14 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rails/engine"
3
+ require 'rails/engine'
4
4
 
5
5
  module ViewComponentReducible
6
6
  # Rails engine for mounting dispatch routes.
7
7
  class Engine < ::Rails::Engine
8
8
  isolate_namespace ViewComponentReducible
9
-
10
- routes.draw do
11
- post "/dispatch", to: "dispatch#call"
12
- end
13
9
  end
14
10
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "json"
3
+ require 'json'
4
4
 
5
5
  module ViewComponentReducible
6
6
  # View helpers for dispatching messages to the VCR endpoint.
@@ -17,20 +17,21 @@ module ViewComponentReducible
17
17
  # @param state [String] signed state token
18
18
  # @param msg_type [String]
19
19
  # @param msg_payload [Hash, String]
20
- # @param target_path [String]
20
+ # @param target_path [String, nil]
21
21
  # @param url [String]
22
22
  # @yield block for the form body (e.g., submit button)
23
23
  # @return [String]
24
- def vcr_dispatch_form(state:, msg_type:, msg_payload: {}, target_path: "root", url: "/vcr/dispatch", &block)
24
+ def vcr_dispatch_form(state:, msg_type:, msg_payload: {}, target_path: nil, url: '/vcr/dispatch', &block)
25
25
  payload = msg_payload.is_a?(String) ? msg_payload : JSON.generate(msg_payload)
26
+ resolved_target = target_path || vcr_envelope_path || instance_variable_get(:@vcr_current_path) || 'root'
26
27
 
27
28
  form_tag(url, method: :post, data: { vcr_form: true }) do
28
29
  body = [
29
- hidden_field_tag("vcr_state", state),
30
- hidden_field_tag("vcr_msg_type", msg_type),
31
- hidden_field_tag("vcr_msg_payload", payload),
32
- hidden_field_tag("vcr_target_path", target_path),
33
- (block_given? ? capture(&block) : "")
30
+ hidden_field_tag('vcr_state', state),
31
+ hidden_field_tag('vcr_msg_type', msg_type),
32
+ hidden_field_tag('vcr_msg_payload', payload),
33
+ hidden_field_tag('vcr_target_path', resolved_target),
34
+ (block_given? ? capture(&block) : '')
34
35
  ]
35
36
  safe_join(body)
36
37
  end
@@ -71,9 +72,12 @@ module ViewComponentReducible
71
72
  current.replaceWith(newNode);
72
73
  }
73
74
  if (payload.state) {
74
- document.querySelectorAll('input[name="vcr_state"]').forEach(function(input) {
75
- input.value = payload.state;
76
- });
75
+ var boundary = document.querySelector('[data-vcr-path="' + targetPath + '"]');
76
+ if (boundary) {
77
+ boundary.querySelectorAll('input[name="vcr_state"]').forEach(function(input) {
78
+ input.value = payload.state;
79
+ });
80
+ }
77
81
  }
78
82
  });
79
83
  });
@@ -81,5 +85,11 @@ module ViewComponentReducible
81
85
  JS
82
86
  content_tag(:script, js.html_safe)
83
87
  end
88
+
89
+ def vcr_envelope_path
90
+ return unless respond_to?(:vcr_envelope) && vcr_envelope
91
+
92
+ vcr_envelope['path']
93
+ end
84
94
  end
85
95
  end
@@ -1,18 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "json"
3
+ require 'json'
4
4
 
5
5
  module ViewComponentReducible
6
6
  # Message payload sent from the client.
7
7
  Msg = Struct.new(:type, :payload, keyword_init: true) do
8
+ # Enable pattern matching with normalized symbol types.
9
+ # @param keys [Array<Symbol>, nil]
10
+ # @return [Hash{Symbol=>Object}]
11
+ def deconstruct_keys(keys)
12
+ payload_hash = { type: normalized_type, payload: payload }
13
+ keys ? payload_hash.slice(*keys) : payload_hash
14
+ end
15
+
8
16
  # Build a Msg from request params.
9
17
  # @param params [Hash]
10
18
  # @return [ViewComponentReducible::Msg]
11
19
  def self.from_params(params)
12
- type = params.fetch("vcr_msg_type")
13
- payload_json = params["vcr_msg_payload"]
14
- payload = payload_json && payload_json != "" ? JSON.parse(payload_json) : {}
20
+ type = params.fetch('vcr_msg_type')
21
+ payload_json = params['vcr_msg_payload']
22
+ payload = payload_json && payload_json != '' ? JSON.parse(payload_json) : {}
15
23
  new(type:, payload:)
16
24
  end
25
+
26
+ private
27
+
28
+ def normalized_type
29
+ type.to_s
30
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
31
+ .tr('-', '_')
32
+ .downcase
33
+ .to_sym
34
+ end
17
35
  end
18
36
  end
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rails/railtie"
3
+ require 'rails/railtie'
4
4
 
5
5
  module ViewComponentReducible
6
6
  # Railtie for wiring helpers into ActionView.
7
7
  class Railtie < ::Rails::Railtie
8
- initializer "view_component_reducible.helpers" do
8
+ initializer 'view_component_reducible.helpers' do
9
9
  ActiveSupport.on_load(:action_view) do
10
10
  include ViewComponentReducible::Helpers
11
11
  end
@@ -11,7 +11,7 @@ module ViewComponentReducible
11
11
  # @param controller [ActionController::Base]
12
12
  # @return [Array<Hash, String>] [new_envelope, html]
13
13
  def call(envelope:, msg:, target_path:, controller:)
14
- root_klass = ViewComponentReducible.registry.fetch(envelope["root"])
14
+ root_klass = ViewComponentReducible.registry.fetch(envelope['root'])
15
15
  new_env = deep_dup(envelope)
16
16
 
17
17
  new_env = dispatch_to_path(root_klass, new_env, msg, target_path, controller)
@@ -25,7 +25,7 @@ module ViewComponentReducible
25
25
  # @param controller [ActionController::Base]
26
26
  # @return [String]
27
27
  def render_target(envelope:, target_path:, controller:)
28
- root_klass = ViewComponentReducible.registry.fetch(envelope["root"])
28
+ root_klass = ViewComponentReducible.registry.fetch(envelope['root'])
29
29
  component_klass, env = find_env_and_class(root_klass, envelope, target_path)
30
30
  controller.view_context.render(component_klass.new(vcr_envelope: env))
31
31
  end
@@ -33,12 +33,12 @@ module ViewComponentReducible
33
33
  private
34
34
 
35
35
  def dispatch_to_path(root_klass, env, msg, target_path, controller)
36
- if target_path == env["path"]
36
+ if target_path == env['path']
37
37
  apply_reducer(root_klass, env, msg, controller)
38
38
  else
39
- child = env["children"].fetch(target_path) { raise KeyError, "Unknown path: #{target_path}" }
40
- child_klass = ViewComponentReducible.registry.fetch(child["root"])
41
- env["children"][target_path] = dispatch_to_path(child_klass, child, msg, target_path, controller)
39
+ child = env['children'].fetch(target_path) { raise KeyError, "Unknown path: #{target_path}" }
40
+ child_klass = ViewComponentReducible.registry.fetch(child['root'])
41
+ env['children'][target_path] = dispatch_to_path(child_klass, child, msg, target_path, controller)
42
42
  env
43
43
  end
44
44
  end
@@ -46,12 +46,11 @@ module ViewComponentReducible
46
46
  def apply_reducer(component_klass, env, msg, controller)
47
47
  component = component_klass.new(vcr_envelope: env)
48
48
  schema = component_klass.vcr_state_schema
49
- data, meta = schema.build(env["data"], env["meta"])
50
- state = { "data" => data, "meta" => meta }
49
+ state = schema.build_data(env['data'])
51
50
 
52
- new_state, effects = component.reduce(state, msg)
53
- env["data"] = new_state["data"]
54
- env["meta"] = new_state["meta"]
51
+ new_state = component.reduce(state, msg)
52
+ env['data'] = normalize_state(new_state, schema)
53
+ effects = build_effects(component, schema, env['data'], msg)
55
54
 
56
55
  run_effects(component_klass, env, effects, controller)
57
56
  end
@@ -64,19 +63,18 @@ module ViewComponentReducible
64
63
 
65
64
  while (eff = effects_queue.shift)
66
65
  steps += 1
67
- raise "Too many effect steps" if steps > MAX_EFFECT_STEPS
66
+ raise 'Too many effect steps' if steps > MAX_EFFECT_STEPS
68
67
 
69
68
  follow_msg = eff.call(controller: controller, envelope: env)
70
69
  next unless follow_msg
71
70
 
72
71
  component = component_klass.new(vcr_envelope: env)
73
72
  schema = component_klass.vcr_state_schema
74
- data, meta = schema.build(env["data"], env["meta"])
75
- state = { "data" => data, "meta" => meta }
73
+ state = schema.build_data(env['data'])
76
74
 
77
- new_state, new_effects = component.reduce(state, follow_msg)
78
- env["data"] = new_state["data"]
79
- env["meta"] = new_state["meta"]
75
+ new_state = component.reduce(state, follow_msg)
76
+ env['data'] = normalize_state(new_state, schema)
77
+ new_effects = build_effects(component, schema, env['data'], follow_msg)
80
78
  effects_queue.concat(Array(new_effects))
81
79
  end
82
80
 
@@ -88,13 +86,30 @@ module ViewComponentReducible
88
86
  end
89
87
 
90
88
  def find_env_and_class(component_klass, env, target_path)
91
- return [component_klass, env] if target_path == env["path"]
89
+ return [component_klass, env] if target_path == env['path']
92
90
 
93
- child = env["children"].fetch(target_path) { raise KeyError, "Unknown path: #{target_path}" }
94
- child_klass = ViewComponentReducible.registry.fetch(child["root"])
91
+ child = env['children'].fetch(target_path) { raise KeyError, "Unknown path: #{target_path}" }
92
+ child_klass = ViewComponentReducible.registry.fetch(child['root'])
95
93
  find_env_and_class(child_klass, child, target_path)
96
94
  end
97
95
 
96
+ def normalize_state(state, schema)
97
+ if state.is_a?(schema.data_class)
98
+ state.to_h.transform_keys(&:to_s)
99
+ elsif state.is_a?(Hash)
100
+ schema.build(state)
101
+ else
102
+ raise ArgumentError, "Reducer must return a Hash or #{schema.data_class}"
103
+ end
104
+ end
105
+
106
+ def build_effects(component, schema, state_hash, msg)
107
+ return [] unless component.respond_to?(:effects)
108
+
109
+ state = schema.build_data(state_hash)
110
+ Array(component.effects(state, msg))
111
+ end
112
+
98
113
  def deep_dup(obj)
99
114
  Marshal.load(Marshal.dump(obj))
100
115
  end
@@ -28,20 +28,12 @@ module ViewComponentReducible
28
28
  class Builder
29
29
  def initialize(schema) = (@schema = schema)
30
30
 
31
- # Define a data field.
31
+ # Define a state field.
32
32
  # @param name [Symbol]
33
33
  # @param default [Object, #call]
34
34
  # @return [void]
35
35
  def field(name, default:)
36
- @schema.add_field(name, default:, kind: :data)
37
- end
38
-
39
- # Define a meta field.
40
- # @param name [Symbol]
41
- # @param default [Object, #call]
42
- # @return [void]
43
- def meta(name, default:)
44
- @schema.add_field(name, default:, kind: :meta)
36
+ @schema.add_field(name, default:)
45
37
  end
46
38
  end
47
39
  end
@@ -8,16 +8,15 @@ module ViewComponentReducible
8
8
  # @param root_component_klass [Class]
9
9
  # @param path [String]
10
10
  # @return [Hash{String=>Object}]
11
- def self.initial(root_component_klass, path: "root")
11
+ def self.initial(root_component_klass, path: 'root')
12
12
  schema = root_component_klass.vcr_state_schema
13
- data, meta = schema.defaults
13
+ data = schema.defaults
14
14
  {
15
- "v" => 1,
16
- "root" => root_component_klass.vcr_id,
17
- "path" => path,
18
- "data" => data,
19
- "children" => {},
20
- "meta" => meta
15
+ 'v' => 1,
16
+ 'root' => root_component_klass.vcr_id,
17
+ 'path' => path,
18
+ 'data' => data,
19
+ 'children' => {}
21
20
  }
22
21
  end
23
22
  end
@@ -4,30 +4,48 @@ module ViewComponentReducible
4
4
  module State
5
5
  # State schema for defining default fields and building state payloads.
6
6
  class Schema
7
- Field = Struct.new(:name, :default, :kind, keyword_init: true) # kind: :data or :meta
7
+ Field = Struct.new(:name, :default, keyword_init: true)
8
8
 
9
9
  def initialize
10
10
  @fields = []
11
+ @data_class = nil
12
+ @data_class_fields = nil
11
13
  end
12
14
 
13
15
  # Add a field definition.
14
16
  # @param name [Symbol]
15
17
  # @param default [Object, #call]
16
- # @param kind [Symbol] :data or :meta
17
18
  # @return [void]
18
- def add_field(name, default:, kind:)
19
- @fields << Field.new(name:, default:, kind:)
19
+ def add_field(name, default:)
20
+ @fields << Field.new(name:, default:)
21
+ end
22
+
23
+ # @return [Class]
24
+ def data_class
25
+ field_names = @fields.map(&:name)
26
+ return @data_class if @data_class && @data_class_fields == field_names
27
+
28
+ defaults_proc = -> { build({}) }
29
+ @data_class_fields = field_names
30
+ @data_class = Data.define(*field_names) do
31
+ def [](key)
32
+ to_h[key.to_sym]
33
+ end
34
+
35
+ define_method(:with_defaults) do
36
+ defaults = defaults_proc.call.transform_keys(&:to_sym)
37
+ self.class.new(**defaults)
38
+ end
39
+ end
20
40
  end
21
41
 
22
42
  # Build state hashes from input payloads.
23
- # @param data_hash [Hash]
24
- # @param meta_hash [Hash]
25
- # @return [Array<Hash{String=>Object}>] [data, meta]
26
- def build(data_hash, meta_hash)
43
+ # @param state_hash [Hash]
44
+ # @return [Hash{String=>Object}]
45
+ def build(state_hash)
27
46
  data = {}
28
- meta = {}
29
47
  @fields.each do |field|
30
- src = (field.kind == :meta ? meta_hash : data_hash) || {}
48
+ src = state_hash || {}
31
49
  value = if src.key?(field.name.to_s)
32
50
  src[field.name.to_s]
33
51
  elsif src.key?(field.name)
@@ -36,19 +54,23 @@ module ViewComponentReducible
36
54
  value = field.default.call if value.nil? && field.default.respond_to?(:call)
37
55
  value = field.default if value.nil? && !field.default.respond_to?(:call)
38
56
 
39
- if field.kind == :meta
40
- meta[field.name.to_s] = value
41
- else
42
- data[field.name.to_s] = value
43
- end
57
+ data[field.name.to_s] = value
44
58
  end
45
- [data, meta]
59
+ data
60
+ end
61
+
62
+ # Build a Data object from input payloads.
63
+ # @param state_hash [Hash]
64
+ # @return [Data]
65
+ def build_data(state_hash)
66
+ data = build(state_hash)
67
+ data_class.new(**data.transform_keys(&:to_sym))
46
68
  end
47
69
 
48
70
  # Build default state hashes.
49
- # @return [Array<Hash{String=>Object}>] [data, meta]
71
+ # @return [Hash{String=>Object}]
50
72
  def defaults
51
- build({}, {})
73
+ build({})
52
74
  end
53
75
  end
54
76
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ViewComponentReducible
4
- VERSION = "0.1.1"
4
+ VERSION = '0.1.2'
5
5
  end
@@ -1,21 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "view_component_reducible/version"
4
- require_relative "view_component_reducible/configuration"
5
- require_relative "view_component_reducible/msg"
6
- require_relative "view_component_reducible/adapter/base"
7
- require_relative "view_component_reducible/adapter/hidden_field"
8
- require_relative "view_component_reducible/adapter/session"
9
- require_relative "view_component_reducible/helpers"
10
- require_relative "view_component_reducible/state/schema"
11
- require_relative "view_component_reducible/state/dsl"
12
- require_relative "view_component_reducible/state/envelope"
13
- require_relative "view_component_reducible/component"
14
- require_relative "view_component_reducible/runtime"
15
- require_relative "view_component_reducible/dispatch"
16
- require_relative "view_component_reducible/dispatch_controller"
17
- require_relative "view_component_reducible/engine"
18
- require_relative "view_component_reducible/railtie"
3
+ require_relative 'view_component_reducible/version'
4
+ require_relative 'view_component_reducible/configuration'
5
+ require_relative 'view_component_reducible/msg'
6
+ require_relative 'view_component_reducible/adapter/base'
7
+ require_relative 'view_component_reducible/adapter/hidden_field'
8
+ require_relative 'view_component_reducible/adapter/session'
9
+ require_relative 'view_component_reducible/helpers'
10
+ require_relative 'view_component_reducible/state/schema'
11
+ require_relative 'view_component_reducible/state/dsl'
12
+ require_relative 'view_component_reducible/state/envelope'
13
+ require_relative 'view_component_reducible/component'
14
+ require_relative 'view_component_reducible/runtime'
15
+ require_relative 'view_component_reducible/dispatch'
16
+ require_relative 'view_component_reducible/dispatch_controller'
17
+ require_relative 'view_component_reducible/engine'
18
+ require_relative 'view_component_reducible/railtie'
19
19
 
20
20
  module ViewComponentReducible
21
21
  class Error < StandardError; end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: view_component_reducible
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - manabeai
@@ -37,6 +37,20 @@ dependencies:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
39
  version: '6.1'
40
+ - !ruby/object:Gem::Dependency
41
+ name: nokogiri
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '1.14'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '1.14'
40
54
  - !ruby/object:Gem::Dependency
41
55
  name: railties
42
56
  requirement: !ruby/object:Gem::Requirement
@@ -108,7 +122,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
108
122
  requirements:
109
123
  - - ">="
110
124
  - !ruby/object:Gem::Version
111
- version: 3.1.0
125
+ version: 3.2.0
112
126
  required_rubygems_version: !ruby/object:Gem::Requirement
113
127
  requirements:
114
128
  - - ">="