Class: Lutaml::KeyValue::Transformation::RuleCompiler

Inherits:
Object
  • Object
show all
Defined in:
lib/lutaml/key_value/transformation/rule_compiler.rb

Overview

Compiles mapping DSL rules into pre-compiled transformation rules.

This is an independent class with explicit dependencies that can be tested in isolation from Transformation.

Examples:

Basic usage

compiler = RuleCompiler.new(
  model_class: MyModel,
  register_id: :default,
  format: :json,
  transformation_factory: ->(type_class) { Transformation.new(type_class, ...) }
)
rules = compiler.compile(mapping_dsl)

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(model_class:, register_id:, format:, transformation_factory:) ⇒ RuleCompiler

Initialize the RuleCompiler with explicit dependencies.

Parameters:

  • model_class (Class)

    The model class

  • register_id (Symbol, nil)

    The register ID

  • format (Symbol)

    The serialization format

  • transformation_factory (Proc)

    Factory lambda ->(type_class) { Transformation }



39
40
41
42
43
44
45
# File 'lib/lutaml/key_value/transformation/rule_compiler.rb', line 39

def initialize(model_class:, register_id:, format:,
transformation_factory:)
  @model_class = model_class
  @register_id = register_id
  @format = format
  @transformation_factory = transformation_factory
end

Instance Attribute Details

#formatSymbol (readonly)

Returns The serialization format (:json, :yaml, :toml).

Returns:

  • (Symbol)

    The serialization format (:json, :yaml, :toml)



28
29
30
# File 'lib/lutaml/key_value/transformation/rule_compiler.rb', line 28

def format
  @format
end

#model_classClass (readonly)

Returns The model class being compiled.

Returns:

  • (Class)

    The model class being compiled



22
23
24
# File 'lib/lutaml/key_value/transformation/rule_compiler.rb', line 22

def model_class
  @model_class
end

#register_idSymbol? (readonly)

Returns The register ID for attribute lookup.

Returns:

  • (Symbol, nil)

    The register ID for attribute lookup



25
26
27
# File 'lib/lutaml/key_value/transformation/rule_compiler.rb', line 25

def register_id
  @register_id
end

#transformation_factoryProc (readonly)

Returns Factory lambda for creating child transformations.

Returns:

  • (Proc)

    Factory lambda for creating child transformations



31
32
33
# File 'lib/lutaml/key_value/transformation/rule_compiler.rb', line 31

def transformation_factory
  @transformation_factory
end

Instance Method Details

#compile(mapping_dsl) ⇒ Array<CompiledRule>

Compile key-value mapping DSL into pre-compiled rules.

This is the main entry point for rule compilation.

Parameters:

  • mapping_dsl (Mapping::KeyValueMapping)

    The mapping to compile

Returns:

  • (Array<CompiledRule>)

    Array of compiled transformation rules



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/lutaml/key_value/transformation/rule_compiler.rb', line 53

def compile(mapping_dsl)
  return [] unless mapping_dsl

  rules = []

  # Compile all mappings (key-value formats don't distinguish elements/attributes)
  mappings_to_compile = if @register_id && @register_id != :default &&
      mapping_dsl.mappings(@register_id).any?
                          mapping_dsl.mappings(@register_id)
                        else
                          mapping_dsl.mappings
                        end
  mappings_to_compile.each do |mapping_rule|
    rule = compile_rule(mapping_rule, mapping_dsl)
    rules << rule if rule
  end

  rules.compact
end

#compile_rule(mapping_rule, mapping_dsl) ⇒ CompiledRule?

Compile a single mapping rule.

Parameters:

  • mapping_rule (Mapping::KeyValueMappingRule)

    The mapping rule

  • mapping_dsl (Mapping::KeyValueMapping)

    The mapping DSL (for accessing key_mappings)

Returns:

  • (CompiledRule, nil)

    Compiled rule or nil



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'lib/lutaml/key_value/transformation/rule_compiler.rb', line 78

def compile_rule(mapping_rule, mapping_dsl)
  # Access custom_methods and delegate early to check how to compile this rule
  custom_methods = mapping_rule.custom_methods
  delegate = mapping_rule.delegate

  attr_name = mapping_rule.to

  # For rules with custom methods but no 'to' attribute (e.g., with: { to: ... }),
  # we need to find the attribute name from the mapping
  if attr_name.nil? && !custom_methods.empty?
    # Try to infer attribute name from 'name' or 'from'
    # For multiple_mappings, name is an array - check each element
    attr_name = if mapping_rule.name
                  names = mapping_rule.name.is_a?(Array) ? mapping_rule.name : [mapping_rule.name]
                  names.map(&:to_sym).find do |n|
                    model_class.attributes(register_id)&.key?(n)
                  end
                elsif mapping_rule.from.is_a?(String) && model_class.attributes(register_id)&.key?(mapping_rule.from.to_sym)
                  mapping_rule.from.to_sym
                end
  end

  # For custom methods without an inferred attribute, use a placeholder
  # The custom method will handle all serialization logic
  if attr_name.nil? && !custom_methods.empty?
    # Use serialized name as placeholder for attribute name
    # The custom method handles everything, so we don't need a real attribute
    # For multiple_mappings, use the first name element
    first_name = if mapping_rule.name.is_a?(Array)
                   mapping_rule.name.first
                 else
                   mapping_rule.name
                 end
    attr_name = first_name&.to_sym || :__custom_method__

    # Create a dummy attribute type for custom methods
    attr_type = nil
    child_transformation = nil
    collection_info = nil
    value_transformer = nil
  else
    return nil unless attr_name

    # For delegated attributes, get attribute from delegated object's class
    if delegate
      # Get the delegate attribute from model to find the delegated class
      delegate_attr = model_class.attributes(register_id)&.[](delegate)
      return nil unless delegate_attr

      # Get the delegated class type
      delegated_class = delegate_attr.type(register_id)
      return nil unless delegated_class

      # Get the actual attribute from the delegated class
      attr = delegated_class.attributes&.[](attr_name)
    else
      # Get attribute definition from model class
      attr = model_class.attributes(register_id)&.[](attr_name)
    end
    return nil unless attr

    # Get attribute type
    attr_type = attr.type(register_id)

    # Build child transformation for nested models
    child_transformation = if attr_type.is_a?(Class) &&
        attr_type < Lutaml::Model::Serialize
                             build_child_transformation(attr_type)
                           end

    # Build collection info (include child_mappings for keyed collections)
    collection_info = if attr.collection?
                        info = { range: attr.options[:collection] }
                        # Add child_mappings if present (for map_key and map_value features)
                        # The keyed collection info might be stored in different places:
                        # 1. As child_mappings on the rule (from map_to_instance)
                        # 2. As @key_mappings on the mapping_dsl (separate __key_mapping entry)
                        # 3. As @value_mappings on the mapping_dsl (from map_value)
                        child_mappings_value = nil

                        # First try to get child_mappings from the rule
                        if mapping_rule.child_mappings
                          child_mappings_value = mapping_rule.child_mappings
                        elsif mapping_rule.hash_mappings
                          child_mappings_value = mapping_rule.hash_mappings
                        end

                        # If not found on the rule, check the mapping_dsl for key_mapping or value_mapping
                        if child_mappings_value.nil?
                          # Check for key_mapping (from map_key)
                          key_mappings = mapping_dsl.key_mapping
                          if key_mappings && !key_mappings.empty?
                            # The key_mappings has :to_instance which tells us which attribute is the key
                            to_instance = key_mappings[:to_instance]
                            if to_instance
                              # Create the child_mappings hash format: { id: :key }
                              child_mappings_value = { to_instance.to_sym => :key }
                            end
                          end

                          # Check for value_mapping (from map_value)
                          if child_mappings_value.nil?
                            value_mappings = mapping_dsl.value_mapping
                            if value_mappings && !value_mappings.empty?
                              # value_mappings is already in the correct format: { attr_name => :value }
                              child_mappings_value = value_mappings
                            end
                          end
                        end

                        if child_mappings_value
                          info[:child_mappings] =
                            child_mappings_value
                        end
                        info
                      end

    # Build value transformer (use delegate_attr for delegated attributes)
    value_transformer = build_value_transformer(mapping_rule,
                                                delegate ? delegate_attr : attr)
  end

  # Access value_map directly
  value_map = mapping_rule.raw_value_map

  # Check if this is a raw mapping (map_all directive)
  is_raw_mapping = mapping_rule.raw_mapping?

  # Get serialized name (key name in output)
  # For raw mappings, serialized_name is nil (content is merged directly)
  serialized_name = if is_raw_mapping
                      nil # Raw content has no key name
                    elsif !mapping_rule.name.nil?
                      # For multiple_mappings, use first element as serialized name
                      mapping_rule.name.is_a?(Array) ? mapping_rule.name.first.to_s : mapping_rule.name.to_s
                    elsif !mapping_rule.from.nil?
                      # For compatibility with multiple_mappings
                      mapping_rule.from.is_a?(Array) ? mapping_rule.from.first.to_s : mapping_rule.from.to_s
                    else
                      attr_name.to_s
                    end

  Lutaml::Model::CompiledRule.new(
    attribute_name: attr_name,
    serialized_name: serialized_name,
    attribute_type: attr_type,
    child_transformation: child_transformation,
    value_transformer: value_transformer,
    collection_info: collection_info,
    mapping_type: is_raw_mapping ? :raw : :key_value,
    render_default: mapping_rule.render_default,
    value_map: value_map,
    custom_methods: custom_methods,
    delegate: delegate,
    root_mappings: mapping_rule.root_mappings,
  )
end

#valid_mapping?(rule, options) ⇒ Boolean

Check if a mapping rule should be applied based on only/except options.

Parameters:

  • rule (CompiledRule)

    The rule to check

  • options (Hash)

    Transformation options (may contain :only, :except)

Returns:

  • (Boolean)

    true if the rule should be applied



241
242
243
244
245
246
247
248
# File 'lib/lutaml/key_value/transformation/rule_compiler.rb', line 241

def valid_mapping?(rule, options)
  only = options[:only]
  except = options[:except]
  name = rule.attribute_name

  (except.nil? || !except.include?(name)) &&
    (only.nil? || only.include?(name))
end