Module: CML::TagLogic

Included in:
Tag
Defined in:
lib/cml/logic.rb

Overview

Logic behavior included in CML::Tag

Defined Under Namespace

Classes: Error, NodeNotFound

Constant Summary collapse

Or =
'||'.freeze
And =
'&&'.freeze
CombinatorDefault =
Or
CombinatorDict =
{
  '||' => Or,
  '++' => And
}
CombinatorExp =
'(\+\+||\|\||)'.freeze
OrCombinatorExp =
'(\|\||)'.freeze
GroupExp =
'\(([^\(\)]+)\)'.freeze
AndPhraseExp =
'([^\(\)\|]+\+\+[^\(\)\|]+)'.freeze
TokenExp =
'([^\(\)\+\|]+)'.freeze
PrecedenceRegexp =
/#{CombinatorExp}#{GroupExp}|#{CombinatorExp}#{AndPhraseExp}|#{CombinatorExp}#{TokenExp}/
TokenRegexp =
/#{CombinatorExp}#{TokenExp}/

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#errorsObject

Returns the value of attribute errors.



147
148
149
# File 'lib/cml/logic.rb', line 147

def errors
  @errors
end

#has_grouped_logicObject

Returns the value of attribute has_grouped_logic.



147
148
149
# File 'lib/cml/logic.rb', line 147

def has_grouped_logic
  @has_grouped_logic
end

#has_liquid_logicObject

Returns the value of attribute has_liquid_logic.



147
148
149
# File 'lib/cml/logic.rb', line 147

def has_liquid_logic
  @has_liquid_logic
end

#logic_treeObject

Returns the value of attribute logic_tree.



147
148
149
# File 'lib/cml/logic.rb', line 147

def logic_tree
  @logic_tree
end

Class Method Details

.parse_expression(logic_expression) ⇒ Object



258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'lib/cml/logic.rb', line 258

def self.parse_expression(logic_expression)
  parsed_expression = []
  logic_expression.scan( PrecedenceRegexp ) do |group_combinator, group_phrase, and_combinator, and_phrase, combinator, token|
    if group_phrase
      parsed_expression << [group_combinator, parse_expression(group_phrase)]
    elsif and_phrase
      ands = []
      and_phrase.scan( TokenRegexp ) do |precedence_combinator, precedence_token|
        ands << [precedence_combinator, precedence_token]
      end
      parsed_expression << [and_combinator, ands]
    else
      parsed_expression << [combinator, token]
    end
  end
  parsed_expression
end

.resolve_combinator(parsed_expression, i, parent_combinator = nil) ⇒ Object

Return the effective boolean combinator for the given index in the parsed logic.

Raises:

  • (RuntimeError)


230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/cml/logic.rb', line 230

def self.resolve_combinator(parsed_expression, i, parent_combinator=nil)
  combinator = parsed_expression[i] && parsed_expression[i][0]
  selected = if (combinator.nil? || combinator.size==0) && parent_combinator
    CombinatorDict[parent_combinator]
  elsif !combinator.nil? && combinator.size>0
    # Use the current phrase's combinator.
    CombinatorDict[combinator]
  else
    if parsed_expression[i+1]
      # Use the next phrase's combinator
      CombinatorDict[ parsed_expression[i+1][0] ]
    else
      CombinatorDefault
    end
  end
  raise(RuntimeError, "Combinator for index #{i} could not be selected from: #{parsed_expression.inspect}") if 
    selected.nil? || selected.size==0
  selected
end

Instance Method Details

#dependencies_on_fields(&block) ⇒ Object

A hash of the Tag’s dependencies with “only-if” logic.

Resolves indexed logic selector keys (e.g. ‘only-if=“pick_a_number:”`) into actual values from parents.

The optional block can be used as a flat collector of all fields; its return value is ignored.



283
284
285
286
287
288
289
290
291
292
293
294
295
296
# File 'lib/cml/logic.rb', line 283

def dependencies_on_fields(&block)
  return @dependencies_on_fields if @dependencies_on_fields && !block_given?
  @dependencies_on_fields = {}
  
  keep_merge!(dependencies_through_cml_group(&block), @dependencies_on_fields)
  return @dependencies_on_fields if only_if.empty? || detect_liquid_logic(only_if) || @logic_tree.nil?
  
  detect_grouped_logic(only_if)
  
  expanded = expand_logic(only_if, &block)
  
  keep_merge!(expanded, @dependencies_on_fields)
  @dependencies_on_fields
end

#dependencies_through_cml_group(&block) ⇒ Object

The <cml::group> only-if logic dependencies

The optional block can be used as a flat collector of all fields; its return value is ignored.



315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
# File 'lib/cml/logic.rb', line 315

def dependencies_through_cml_group(&block)
  return @dependencies_through_cml_group if @dependencies_through_cml_group
  @dependencies_through_cml_group = {}
  
  @logic_tree.parser.each_cml_group_descendant do |group_node, cml_tag, i|
    next if group_node.attributes['only-if'].nil? || 
      group_node.attributes['only-if'].value.size <= 0 || 
        self != cml_tag
    group_only_if = group_node.attributes['only-if'].value.to_s
    
    expanded = expand_logic(group_only_if, &block)
    keep_merge!(expanded, @dependencies_through_cml_group)
  end
  
  @dependencies_through_cml_group
end

#depends_on_fieldsObject

Returns array of field names that this tag depends on with “only-if” logic.



300
301
302
303
304
305
306
307
308
309
# File 'lib/cml/logic.rb', line 300

def depends_on_fields
  return @depends_on_fields if @depends_on_fields
  @depends_on_fields = []
  
  dependencies_on_fields do |name, descs|
    @depends_on_fields << name
  end
  
  @depends_on_fields
end

#describe_logic_token(logic_token) ⇒ Object

For an only-if value, e.g, ‘omg`, `!omg`, `omg:`, `omg:`, etc., return an array of the referenced name & an array of hashes of logic properties.



334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
# File 'lib/cml/logic.rb', line 334

def describe_logic_token(logic_token)
  field_name, match_key = logic_token.split( ":" )
  
  # detect NOT logic
  is_not = if /^!/===field_name
    field_name = field_name.gsub( /^!/, '' )
    true
  else
    false
  end
  
  unless @logic_tree.nodes[field_name]
    raise CML::TagLogic::NodeNotFound, "CML element '#{name}' contains only-if logic that references a missing field '#{field_name}'."
  end
  
  descs = []
  
  if match_key
    # unwrap the match key from it's square brackets
    match_key = match_key.gsub( /^\[([^\]]+)\]$/, "\\1")
    
    # when an integer index, set `match_key` to the literal value in the parent tag
    if /^\d+$/===match_key 
      unless @logic_tree.nodes[field_name].tag.children
        raise CML::TagLogic::NodeNotFound, "CML element '#{name}' contains only-if logic that references a child index, '#{field_name}:[#{match_key}]', but '#{field_name}' contains no child elements."
      end
      if (tag_at_index = @logic_tree.nodes[field_name].tag.children[$~.to_s.to_i])
        descs << { :is_not => is_not, :match_key => tag_at_index.value }
      end
      
    # when 'unchecked' logic "is not the checkbox's value or any of the checkboxes/checkbox values";
    # is_not logic gets inverted instead of just setting true, just incase we're notting a not
    elsif 'unchecked'==match_key
      if @logic_tree.nodes[field_name].tag.tag == 'checkbox'
        checkbox = @logic_tree.nodes[field_name].tag
        descs << { :is_not => !is_not, :match_key => checkbox.value }
      else
        unless @logic_tree.nodes[field_name].tag.children
          raise CML::TagLogic::NodeNotFound, "CML element '#{name}' contains only-if logic that references a checked value, '#{field_name}:#{match_key}', but '#{field_name}' is not a checkbox nor contains checkboxes."
        end
        @logic_tree.nodes[field_name].tag.children.each do |child|
          next unless child.tag == 'checkbox'
          descs << { :is_not => !is_not, :match_key => child.value }
        end
      end
    end
  end
  
  if descs.empty?
    # When matcher is nil, invert the is_not logic to be semantically accurate.
    #
    # i.e. a nil match key actually means "is not blank,"
    # while a negated nil matcher actually means "is blank"
    #
    is_not_with_nil = match_key.nil? ? !is_not : is_not
    descs << { :is_not => is_not_with_nil, :match_key => match_key }
  end
  
  [field_name, descs]
end

#detect_grouped_logic(only_if) ⇒ Object

Detect parenthetical grouping in only-if logic expressions



396
397
398
399
400
401
# File 'lib/cml/logic.rb', line 396

def detect_grouped_logic(only_if)
  @errors ||= []
  if /\([^\(\)]+\)/===only_if
    @has_grouped_logic = true 
  end
end

#detect_liquid_logic(only_if) ⇒ Object

Detect Liquid tags in only-if logic expressions



404
405
406
407
408
409
410
# File 'lib/cml/logic.rb', line 404

def detect_liquid_logic(only_if)
  @errors ||= []
  if /\{\{/===only_if
    @errors << "The logic tree cannot be constructed when only-if contains a Liquid tag: #{only_if}"
    @has_liquid_logic = true 
  end
end

#each_logic_token_in(logic_expression) ⇒ Object

For each only-if token, e.g, ‘omg`, `!omg`, `omg:`, `omg:`, etc., call the block with args field_name, match_key & is_not.



165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/cml/logic.rb', line 165

def each_logic_token_in(logic_expression)
  return unless block_given?
  logic_expression.split( /\+\+|\|\|/ ).each do |logic_token|
    field_name, match_key = logic_token.split( ":" )
    
    # prune the possibly-dangling open paren from logic grouping
    field_name.gsub!( /^\(/, '' )
    # prune the possibly-dangling close paren from logic grouping
    match_key.gsub!( /\)$/, '') if match_key
    
    # detect NOT logic
    is_not = if /^!/===field_name
      field_name = field_name.gsub( /^!/, '' )
      true
    else
      false
    end
    
    yield field_name, match_key, is_not
  end
end

#expand_logic(only_if, &block) ⇒ Object

Generate the full logic structure for an only-if.

The optional block can be used as a flat collector of all fields; its return value is ignored.



191
192
193
194
# File 'lib/cml/logic.rb', line 191

def expand_logic(only_if, &block)
  parsed = CML::TagLogic.parse_expression(only_if)
  expand_parsed_expression(parsed, &block)
end

#expand_parsed_expression(parsed_expression, &block) ⇒ Object

Generate the full logic structure for a parsed only-if.

The optional block can be used as a flat collector of all fields; its return value is ignored.



200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/cml/logic.rb', line 200

def expand_parsed_expression(parsed_expression, &block)
  expanded_tree = {}
  parsed_expression.each_with_index do |expression_array, i|
    combinator, phrase = expression_array
    combinator_key = CML::TagLogic.resolve_combinator(parsed_expression, i)
    branch = expanded_tree[combinator_key] ||= []
    
    if Array===phrase
      # Recurse for grouped logic
      branch << expand_parsed_expression(phrase, &block)
    else
      name, descs = describe_logic_token(phrase)
      yield(name, descs) if block_given?
      value ||= descs
      branch << { name => value }
    end
  end
  expanded_tree
end

#in_logic_graph?Boolean

Override in Tags classes that should be omitted from the logic graph.

Returns:

  • (Boolean)


155
156
157
# File 'lib/cml/logic.rb', line 155

def in_logic_graph?
  true
end

#keep_merge!(hash, target) ⇒ Object



413
414
415
416
417
418
419
420
421
422
# File 'lib/cml/logic.rb', line 413

def keep_merge!(hash, target)
  hash.keys.each do |key|
    if hash[key].is_a? Hash and self[key].is_a? Hash
      target[key] = target[key].keep_merge(hash[key])
      next
    end
    target.update(hash) { |key, *values| values.flatten.uniq }
  end
  target
end

#only_ifObject



159
160
161
# File 'lib/cml/logic.rb', line 159

def only_if
  @only_if ||= @attrs["only-if"].to_s
end