Module: Ruflet::UI::ControlFactory

Defined in:
lib/ruflet_ui/ruflet/ui/control_factory.rb

Constant Summary collapse

CLASS_MAP =
Services::RufletServices::CLASS_MAP
.merge(Controls::RufletControls::CLASS_MAP)
.freeze
EXTENSION_CLASS_MAP =
{}
CONSTRUCTOR_KEYWORDS_CACHE =
{}
COMMON_ATTRIBUTES =
%i[flip ref transform].freeze
ATTRIBUTE_OVERRIDES =
{
  "expansionpanellist" => %i[auto_scroll on_scroll scroll scroll_interval],
  "ruflet_app" => %i[assets_dir on_python_output],
  "rufletapp" => %i[assets_dir on_python_output],
  "image" => %i[
    align animate_align animate_margin animate_offset animate_opacity animate_position
    animate_rotation animate_scale animate_size aspect_ratio badge bottom col disabled
    expand expand_loose left margin offset on_animation_end on_size_change opacity right
    rotate rtl scale size_change_interval tooltip top visible
  ],
  "navigationrail" => %i[pin_leading_to_top pin_trailing_to_bottom scrollable],
  "responsiverow" => %i[auto_scroll on_scroll scroll scroll_interval],
  "text" => %i[
    align animate_align animate_margin animate_offset animate_opacity animate_position
    animate_rotation animate_scale animate_size aspect_ratio badge bottom col disabled
    expand expand_loose height left margin offset on_animation_end on_size_change opacity
    right rtl scale size_change_interval tooltip top visible width
  ]
}.freeze

Class Method Summary collapse

Class Method Details

.build(type, id: nil, **props) ⇒ Object



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/ruflet_ui/ruflet/ui/control_factory.rb', line 60

def build(type, id: nil, **props)
  normalized_type = type.to_s.downcase
  if ENV["RUFLET_DEBUG"] == "1" && normalized_type == "floatingactionbutton"
    Kernel.warn("[factory] type=#{normalized_type} id=#{id.inspect} props=#{props.inspect}")
  end
  klass = EXTENSION_CLASS_MAP[normalized_type] || CLASS_MAP[normalized_type]
  if klass
    normalized_props, supplemental_props = normalize_constructor_props(klass, normalized_type, props)
    if ENV["RUFLET_DEBUG"] == "1" && normalized_type == "floatingactionbutton"
      Kernel.warn("[factory] normalized_props=#{normalized_props.inspect}")
    end
    control = klass.new(id: id, **normalized_props)
    supplemental_props.each do |key, value|
      if key.to_s.start_with?("on_") && value.respond_to?(:call)
        control.attach_handler(key.to_s.delete_prefix("on_"), value)
        next
      end
      next if key == :ref

      control.props[key.to_s] = normalize_common_value(value)
    end
    return control
  end

  Control.new(type: normalized_type, id: id, **props)
end

.constructor_keywords(klass) ⇒ Object



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/ruflet_ui/ruflet/ui/control_factory.rb', line 104

def constructor_keywords(klass)
  return CONSTRUCTOR_KEYWORDS_CACHE[klass] if CONSTRUCTOR_KEYWORDS_CACHE.key?(klass)

  keywords = if klass.const_defined?(:KEYWORDS)
    klass::KEYWORDS
  else
    klass.instance_method(:initialize).parameters
         .select { |kind, _| kind == :key || kind == :keyreq }
         .map { |_, name| name }
         .reject { |name| name == :id }
  end

  CONSTRUCTOR_KEYWORDS_CACHE[klass] = keywords.freeze
rescue StandardError
  []
end

.dsl_receiver?(receiver) ⇒ Boolean

Where an unregistered extension helper is still meant to build a control: a script's top level, and objects the framework owns. Anything else asking for an unknown method is asking by accident.

Returns:

  • (Boolean)


51
52
53
54
55
56
57
58
# File 'lib/ruflet_ui/ruflet/ui/control_factory.rb', line 51

def dsl_receiver?(receiver)
  return true if receiver.equal?(TOPLEVEL_BINDING.receiver)

  name = receiver.class.name
  name.is_a?(String) && name.start_with?("Ruflet::")
rescue StandardError
  false
end

.known_control?(name) ⇒ Boolean

Whether a name resolves to a control, including extensions registered at runtime. Used to decide when a missing method is a DSL call rather than something Ruby expected to raise.

Returns:

  • (Boolean)


43
44
45
46
# File 'lib/ruflet_ui/ruflet/ui/control_factory.rb', line 43

def known_control?(name)
  key = name.to_s.downcase
  EXTENSION_CLASS_MAP.key?(key) || CLASS_MAP.key?(key)
end

.normalize_common_value(value) ⇒ Object



136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/ruflet_ui/ruflet/ui/control_factory.rb', line 136

def normalize_common_value(value)
  case value
  when Hash
    value.each_with_object({}) do |(key, nested), result|
      result[key.to_s] = normalize_common_value(nested)
    end
  when Array
    value.map { |nested| normalize_common_value(nested) }
  when Symbol
    value.to_s
  else
    value
  end
end

.normalize_constructor_props(klass, type, props) ⇒ Object



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/ruflet_ui/ruflet/ui/control_factory.rb', line 87

def normalize_constructor_props(klass, type, props)
  keywords = constructor_keywords(klass)
  return [props, {}] if keywords.empty? && ATTRIBUTE_OVERRIDES[type].nil?

  allowed = keywords.map(&:to_s)
  mapped = props.each_with_object({}) { |(k, v), out| out[k.to_sym] = v }
  if mapped.key?("value") && !allowed.include?("value") && allowed.include?("text") && !mapped.key?("text")
    mapped["text"] = mapped.delete("value")
  end
  if mapped.key?(:value) && !allowed.include?("value") && allowed.include?("text") && !mapped.key?(:text)
    mapped[:text] = mapped.delete(:value)
  end
  supplemental_names = COMMON_ATTRIBUTES + ATTRIBUTE_OVERRIDES.fetch(type, [])
  supplemental = mapped.slice(*supplemental_names)
  [mapped.reject { |key, _| supplemental_names.include?(key) }, supplemental]
end

.register_extension(type, klass) ⇒ Object

Raises:

  • (ArgumentError)


121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/ruflet_ui/ruflet/ui/control_factory.rb', line 121

def register_extension(type, klass)
  key = type.to_s.downcase
  raise ArgumentError, "Extension control type cannot be empty" if key.empty?
  raise ArgumentError, "Extension control must inherit Ruflet::Control" unless klass <= Ruflet::Control

  existing = EXTENSION_CLASS_MAP[key]
  if existing && existing != klass
    raise ArgumentError, "Extension control `#{key}` is already registered by #{existing}"
  end

  EXTENSION_CLASS_MAP[key] = klass
  CONSTRUCTOR_KEYWORDS_CACHE.delete(klass)
  klass
end