Skip to content

Commit fd5493b

Browse files
committed
Refactored ExtraOptionConfigs to BaseConfiguration pattern with ActiveModel::Validations - fixes #986
1 parent eb965df commit fd5493b

41 files changed

Lines changed: 5189 additions & 523 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/helpers/application_helper.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,9 @@ def show_caption_before(key, captions, mode: nil, no_sub: nil, ignore_missing: t
214214

215215
mode ||= action_name == 'new' ? :new : :edit
216216
caption = captions[key]
217-
caption = caption[:"#{mode}_caption"] || caption[:caption] || '' if caption.is_a?(Hash)
217+
if caption.is_a?(Hash) || caption.is_a?(OptionConfigs::BaseNamedConfiguration)
218+
caption = caption[:"#{mode}_caption"] || caption[:caption] || ''
219+
end
218220
if @form_object_instance && !no_sub
219221
caption = Formatter::Substitution.substitute(caption, data: @form_object_instance, tag_subs: nil,
220222
ignore_missing:)

app/models/concerns/options_handler.rb

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,72 @@ def configure_hash(config_item_name, with:)
172172
ch.const_set(config_item_name.ns_camelize, c)
173173
end
174174

175+
#
176+
# Class method for declaring that a class stores a single direct value of a given type.
177+
# This provides a consistent mechanism for defining configuration classes that hold
178+
# one typed value (e.g., a string, array, hash, or if_condition) rather than
179+
# multiple named attributes.
180+
#
181+
# The type metadata allows future validation and coercion. Currently supported types:
182+
# - :string — stores a String value
183+
# - :array — stores an Array value
184+
# - :hash — stores an arbitrary Hash value
185+
# - :if_condition — stores a conditional Hash (for access/validation conditions)
186+
#
187+
# Example:
188+
# class Label < SomeBase
189+
# configure_direct :label, type: :string
190+
# end
191+
#
192+
# @param [Symbol] config_item_name - the name of the configuration item
193+
# @param [Symbol] type - the value type (:string, :array, :hash, :if_condition)
194+
def configure_direct(config_item_name, type:)
195+
attr_accessor(config_item_name) unless method_defined?(config_item_name)
196+
197+
add_option_type(:direct, config_item_name)
198+
199+
# Store type metadata for future validation/coercion
200+
@direct_types ||= {}
201+
@direct_types[config_item_name] = type
202+
end
203+
204+
#
205+
# Returns the registered direct types for this class.
206+
# @return [Hash{Symbol => Symbol}] mapping of attribute name to type
207+
def direct_types
208+
@direct_types || {}
209+
end
210+
211+
#
212+
# Class method for declaring a typed attribute whose value is an instance of
213+
# a class inheriting from BaseConfiguration.
214+
#
215+
# When the including class is initialized with a hash configuration, the
216+
# attribute value is automatically set by passing the corresponding hash
217+
# entry to the type class constructor.
218+
#
219+
# @param [Symbol] config_item_name - the attribute name
220+
# @param [Class] type - a class inheriting from BaseConfiguration that
221+
# accepts a hash in its constructor
222+
#
223+
# @example
224+
# configure_typed_attribute :creatable_if, type: ExtraOptionConfigs::IfCondition
225+
def configure_typed_attribute(config_item_name, type:)
226+
attr_accessor(config_item_name) unless method_defined?(config_item_name)
227+
228+
add_option_type(:typed, config_item_name)
229+
230+
@typed_attribute_types ||= {}
231+
@typed_attribute_types[config_item_name] = type
232+
end
233+
234+
#
235+
# Returns the registered typed attribute types for this class.
236+
# @return [Hash{Symbol => Class}] mapping of attribute name to type class
237+
def typed_attribute_types
238+
@typed_attribute_types || {}
239+
end
240+
175241
#
176242
# List of configuration items having child options.
177243
# Each represents the name of an accessor attribute in this model
@@ -180,7 +246,9 @@ def option_types
180246
@option_types ||= {
181247
multi: [],
182248
simple: [],
183-
hash: []
249+
hash: [],
250+
direct: [],
251+
typed: []
184252
}
185253
end
186254

@@ -303,6 +371,7 @@ def setup_from_hash_config
303371
setup_all_options_multi hash_configuration
304372
setup_all_options_simple hash_configuration
305373
setup_all_options_hash hash_configuration
374+
setup_all_options_typed hash_configuration
306375

307376
hash_configuration
308377
end
@@ -322,6 +391,14 @@ def setup_all_options_simple(hash_configuration)
322391
end
323392
end
324393

394+
def setup_all_options_typed(hash_configuration)
395+
self.class.option_types[:typed]&.each do |option_type|
396+
type_class = self.class.typed_attribute_types[option_type]
397+
config_val = hash_configuration[option_type]
398+
send("#{option_type}=", type_class.new(config_val))
399+
end
400+
end
401+
325402
def setup_all_options_hash(hash_configuration)
326403
self.class.option_types[:hash].each do |option_type|
327404
setup_options_hash(hash_configuration, option_type)

app/models/option_configs/activity_log_options.rb

Lines changed: 11 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,21 @@ class ActivityLogOptions < ExtraOptions
1010
ValidNfsStoreCanPerformKeys = %i[download_if view_files_as_image_if view_files_as_html_if send_files_to_trash_if
1111
move_files_if user_file_actions_if].freeze
1212

13+
def self.config_class_registry
14+
super.merge(
15+
e_sign: ExtraOptionConfigs::ESignConfig,
16+
nfs_store: ExtraOptionConfigs::NfsStoreConfig
17+
)
18+
end
19+
1320
def self.add_key_attributes
14-
%i[e_sign nfs_store]
21+
[]
1522
end
1623

1724
attr_accessor(*key_attributes)
1825

1926
def initialize(name, config, parent_activity_log)
20-
super(name, config, parent_activity_log)
27+
super
2128

2229
if @config_obj.disabled
2330
Rails.logger.info "configuration for this activity log has not been enabled: #{@config_obj.table_name}"
@@ -26,57 +33,11 @@ def initialize(name, config, parent_activity_log)
2633
raise FphsException, 'extra log options name: property can not be blank' if self.name.blank?
2734

2835
# Activity logs have some predefined captions. Set these up.
29-
if caption_before && !caption_before.is_a?(Hash)
36+
if caption_before && !caption_before.is_a?(Hash) && !caption_before.is_a?(ExtraOptionConfigs::BaseConfiguration)
3037
raise FphsException, 'extra log options caption_before: must be a hash of {field_name: caption, ...}'
3138
end
3239

3340
init_caption_before
34-
35-
clean_e_sign_def
36-
clean_nfs_store_def
37-
end
38-
39-
def clean_nfs_store_def
40-
return unless nfs_store
41-
42-
can_perform = nfs_store[:can]
43-
44-
unless valid_config_keys?(nfs_store, ValidNfsStoreKeys)
45-
failed_config :nfs_store,
46-
"nfs_store contains invalid keys #{nfs_store.keys} - " \
47-
"expected only #{ValidNfsStoreKeys}"
48-
end
49-
50-
unless can_perform.nil? || valid_config_keys?(can_perform, ValidNfsStoreCanPerformKeys)
51-
failed_config :nfs_store__can,
52-
"nfs_store.can contains invalid keys #{can_perform.keys} - " \
53-
"expected only #{ValidNfsStoreCanPerformKeys}"
54-
end
55-
56-
NfsStore::Config::ExtraOptions.clean_def nfs_store
57-
end
58-
59-
def clean_e_sign_def
60-
return unless e_sign
61-
62-
# Set up the structure so that we can use the standard reference methods to parse the configuration
63-
e_sign[:document_reference] = { item: e_sign[:document_reference] } unless e_sign[:document_reference][:item]
64-
e_sign[:document_reference].each_value do |refitem|
65-
# Make all keys singular, to simplify configurations
66-
refitem.transform_keys! do |k|
67-
new_k = k.to_s.singularize.to_sym
68-
end
69-
70-
refitem.each do |mn, conf|
71-
to_class = ModelReference.to_record_class_for_type(mn)
72-
73-
refitem[mn][:to_record_label] = conf[:label] || to_class&.human_name
74-
if to_class&.respond_to?(:no_master_association)
75-
refitem[mn][:no_master_association] = to_class.no_master_association
76-
end
77-
refitem[mn][:to_model_name_us] = to_class&.to_s&.ns_underscore
78-
end
79-
end
8041
end
8142

8243
# A list of all fields defined within all the individual activity definitions. This does not include
@@ -129,7 +90,7 @@ def init_caption_before
12990
protocol_id: {
13091
caption: "Select the protocol this #{curr_name} is related to. A tracker event will be recorded under this protocol."
13192
},
132-
"set_related_#{item_type}_rank".to_sym => {
93+
"set_related_#{item_type}_rank": {
13394
caption: "To change the rank of the related #{item_type.to_s.humanize}, select it:"
13495
}
13596
}

app/models/option_configs/base_named_configuration.rb

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,61 @@ class BaseNamedConfiguration < OptionConfigs::BaseOptions
44

55
attr_accessor :owner, :use_hash_config
66

7+
#
8+
# Hash-like access to configuration attributes by key name.
9+
# Enables backward compatibility with code that expects Hash-like access
10+
# on individual named configuration items, e.g. `named_config[:caption]`
11+
# @param [Symbol | String] key - the attribute name
12+
# @return [Object] the attribute value, or nil if not recognized
13+
def [](key)
14+
sym_key = key.to_sym
15+
return nil unless respond_to?(sym_key)
16+
17+
send(sym_key)
18+
end
19+
20+
#
21+
# Convert all configured attributes to a plain Hash.
22+
# Mirrors OptionsHandler::Configuration#to_h for named configurations.
23+
# @return [Hash{Symbol => Object}]
24+
def to_h
25+
res = {}
26+
self.class.option_types[:simple].each { |k| res[k] = send(k) }
27+
res
28+
end
29+
30+
alias to_hash to_h
31+
32+
# Hash-compatible dig for nested access on named configurations.
33+
# @param keys [Array<Symbol>] nested key path
34+
# @return [Object, nil]
35+
def dig(*keys)
36+
first = keys.shift
37+
val = self[first]
38+
return val if keys.empty? || val.nil?
39+
40+
val.respond_to?(:dig) ? val.dig(*keys) : nil
41+
end
42+
43+
#
44+
# Equality comparison: compare as plain Hash for backward compatibility
45+
# with code that previously stored raw Hashes instead of NamedConfiguration objects.
46+
# @param other [Object] value to compare against
47+
# @return [Boolean]
48+
def ==(other)
49+
return to_h == other if other.is_a?(Hash)
50+
51+
super
52+
end
53+
54+
#
55+
# Return a Hash containing only non-nil attribute values.
56+
# Useful for serialization where nil values should be omitted.
57+
# @return [Hash{Symbol => Object}]
58+
def filtered_hash
59+
to_h.reject { |_k, v| v.nil? }
60+
end
61+
762
def config_text
863
return super unless owner
964

0 commit comments

Comments
 (0)