Class: Ruote::Exp::FilterExpression

Inherits:
FlowExpression show all
Defined in:
lib/ruote/exp/fe_filter.rb

Overview

Filter is a one-way filter expression. It filters workitem fields. Validations and Transformations are possible.

Validations will raise errors (that’ll block the process segment unless an :on_error attribute somehow deals with the problem).

Transformations will copy values around fields.

There are two ways to use it. With a single rule or with an array of rules.

filter 'x', :type => 'string'
  # will raise an error if the field 'x' doesn't contain a String

or

filter :in => [
  { :field => 'x', :type => 'string' },
  { :field => 'y', :type => 'number' }
]

For the remainder of this piece of documentation, the one rule filter will be used.

filtering targets (field names)

Top level field names are OK :

filter 'customer_id', :type => 'string'
filter 'invoice_id', :type => 'number'

Pointing to fields lying deeper is OK :

filter 'customer.id', :type => 'number'
filter 'customer.name', :type => 'string'
filter 'invoice', :type => 'array'
filter 'invoice.0.id', :type => 'number'

(Note the dollar notation is also OK with such dotted identifiers)

It’s possible to target multiple fields by passing a list of field names or a regular expression.

filter 'city, region, country', :type => 'string'
  # will make sure that those 3 fields hold a string value

filter '/^address\.x_/', :type => number
filter '/^address!x_/', :type => number
  # fields whosename start with x_ in the address hash should be numbers

Note the “!” used as a shortcut for “.” in the second line.

Passing a | separated list of field also works :

filter 'city|region|country', :type => 'string'
  # will make sure that at least of one those field is present
  # any of the three that is present must hold a string

validations

‘type’

Ruote is a Ruby library, it adopts Ruby “laissez-faire” for workitem fields, but sometimes, some type oriented validation is necessary. Ruote limits itself to the types found in the JSON specification with one or two additions.

filter 'x', :type => 'string'
filter 'x', :type => 'number'
filter 'x', :type => 'bool'
filter 'x', :type => 'boolean'
filter 'x', :type => 'null'

filter 'x', :type => 'array'

filter 'x', :type => 'object'
filter 'x', :type => 'hash'
  # 'object' and 'hash' are equivalent

It’s OK to pass multiple types for a field

filter 'x', :type => 'bool,number'
filter 'x', :type => [ 'string', 'array' ]

filter 'x', :type => 'string,null'
  # a string or null or not set

The array and the object/hash types accept a subtype for their values (a hash/object must have string keys anyway).

filter 'x', :type => 'array<number>'
filter 'x', :type => 'array<string>'
filter 'x', :type => 'array<array<string>>'

filter 'x', :type => 'array<string,number>'
  # an array of strings or numbers (both)
filter 'x', :type => 'array<string>,array<number>'
  # an array of strings or an array of numbers

‘match’ and ‘smatch’

‘match’ will check if a field, when turned into a string, matches a given regular expression.

filter 'x', :match => '1'
  # will match "11", 1, 1.0, "212"

‘smatch’ works the same but only accepts fields that are strings.

filter 'x', :smatch => '^user_'
  # valid only if x's value is a string that starts with "user_"

‘size’ and ‘empty’

‘size’ is valid for values that respond to the #size method (strings hashes and arrays).

filter 'x', :size => 4
  # will be valid of values like [ 1, 2, 3, 4 ], "toto" or
  # { 'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4 }

filter 'x', :size => [ 4, 5 ]
filter 'x', :size => '4,5'
  # four to five elements

filter 'x', :size => [ 4 ]
filter 'x', :size => [ 4, nil ]
filter 'x', :size => '4,'
  # four or more elements

filter 'x', :size => [ nil, 4 ]
filter 'x', :size => ',4'
  # four elements or less

Similarly, the ‘empty’ check will evaluate to true (ie not raise an exception) if the value responds to #empty? and is, well, not empty.

filter 'x', :empty => true

‘is’

Checks if a field holds the given value.

filter 'x', :is => true
filter 'x', :is => [ 'a', 2, 3 ]

‘in’

Checks if a value is in a given set of values.

filter 'x', :in => [ 1, 2, 3 ]
filter 'x', :in => "john, jeff, jim"

‘has’

Checks if an array contains certain values

filter 'x', :has => 1
filter 'x', :has => "x"
filter 'x', :has => [ 1, 7, 12 ]
filter 'x', :has => "abraham, bob, charly"

Also checks if a hash has certain keys (strings only of course)

filter 'x', :has => "x"
filter 'x', :has => "abraham, bob, charly"

‘includes’

Checks if an array includes a given value. Works with Hash values as well.

filter 'x', :includes => 1

Whereas ‘has’ accepts multiple values, ‘includes’ only accepts one (like Ruby’s Array#include?).

‘valid’

Sometimes, it’s better to immediately say ‘true’ or ‘false’.

filter 'x', :valid => 'true'
filter 'x', :valid => 'false'

Not very useful…

In fact, it’s meant to be used with the dollar notation

filter 'x', :valid => '${other.field}'
  # will be valid if ${other.field} evaluates to 'true'...

cumulating validations

As seen before, type validations can be cumulated.

filter 'x', :type => 'bool,number'

Validations can be cumulated as well

filter 'x', :type => 'array<number>', :has => [ 1, 2 ]
  # will be valid if the field 'x' holds an array of numbers
  # and that array has 1 and 2 among its elements

validation errors

By defaults a validation error will result in a process error (ie the process instance will have to be manually fixed and resumed, or there is a :on_error somewhere dealing automatically with errors).

It’s possible to prevent raising an error and simply record the validation errors.

filter 'x', :type => 'bool,number', :record => true

will enumerate validation errors in the ‘validation_errors’ workitem field.

filter 'y', :type => 'bool,number', :record => 'verrors'

will enumerate validation errors in teh ‘verrors’ workitem field.

To flush the recording field, use :flush => true

sequence do
  filter 'x', :type => 'string', :record => true
  filter 'y', :type => 'number', :record => true, :flush => true
  participant 'after'
end

the participant ‘after’ will only see the result of the second filter.

For complex filters, if the first rule has :record => true, the ‘recording’ will happen for the whole filter.

sequence do
  filter :in => [
    { :field => 'x', :type => 'string', :record => true },
    { :field => 'y', :type => 'number' } ]
  participant 'after'
end

transformations

So far, only the validation aspect of filter was shown. They can also be used to transform the workitem.

filter 'x', :type => 'string', :or => 'missing'
  # will replace the value of x by 'missing' if it's not a string

filter 'z', :remove => true
  # will remove the workitem field z

filter 'a,b,c', 'set' => '---'
  # sets the field a, b and c to '---'

‘remove’

Removes a field (or a subfield).

filter 'z', :remove => true

‘default’

If there is no value for a field, sets it

filter 'x', 'default' => 0
  # will set x to 0, if it's not set or its value is nil

filter '/^user-.+/', 'default' => 'nemo'
  # will set any 'user-...' field to 'nemo' if its value is nil

‘or’

‘or’ combines with a condition. The ‘or’ value is set if the condition evaluates to false.

Using ‘or’ without a condition makes it equivalent to a ‘default’.

filter 'x', 'or' => 0
  # will set x to 0, if it's not set or its value is nil

filter 'x', 'type' => 'number', 'or' => 0
  # if x is not set or is not a number, will set it to 0

Multiple conditions are OK

filter 'x', 't' => 'array', 'has' => 'cat', 'or' => []
  # if x is an array and has the 'cat' element, nothing will happen.
  # Else x will be set to [].

‘and’

‘and’ is much like ‘or’, but it triggers if the condition evaluates to true.

filter 'x', 'type' => number, 'and' => '*removed*'
  # if x is a number, it will replace it with '*removed*'

‘set’

Like ‘remove’ removes unconditionally, ‘set’ sets a field unconditionally.

filter 'x', 'set' => 'blue'
  # sets the field x to 'blue'

copy, merge, migrate / to, from

# in :   { 'x' => 'y' }
filter 'x', 'copy_to' => 'z'
# out :  { 'x' => 'y', 'z' => 'y' }

# in :   { 'x' => 'y' }
filter 'z', 'copy_from' => 'x'
# out :  { 'x' => 'y', 'z' => 'y' }

# in :   { 'x' => 'y' }
filter 'z', 'copy_from' => 'x'
# out :  { 'x' => 'y', 'z' => 'y' }

# in :   { 'a' => %w[ x y ]})
filter '/a\.(.+)/', 'copy_to' => 'b\1'
# out :  { 'a' => %w[ x y ], 'b0' => 'x', 'b1' => 'y' },

# in :   { 'a' => %w[ x y ]})
filter '/a!(.+)/', 'copy_to' => 'b\1'
# out :  { 'a' => %w[ x y ], 'b0' => 'x', 'b1' => 'y' },
  #
  # '!' is used as a replacement for '\.' in regexes

# in :   { 'a' => 'b', 'c' => 'd', 'source' => [ 7 ] })
filter '/^.$/', 'copy_from' => 'source.0'
# out :  { 'a' => 7, 'c' => 7, 'source' => [ 7 ] },

‘copy_to’ and ‘copy_from’ copy whole fields. ‘move_to’ and ‘move_from’ move fields.

‘merge_to’ and ‘merge_from’ merge hashes (or add values to arrays), ‘push_to’ and ‘push_from’ are aliases for ‘merge_to’ and ‘merge_from’ respectively.

‘migrate_to’ and ‘migrate_from’ act like ‘merge_to’ and ‘merge_from’ but delete the merge source afterwards (like ‘move’).

All those hash/array filter operations understand the ‘.’ field, meaning the hash being filtered itself.

# in :   { 'x' => { 'a' => 1, 'b' => 2 } })
filter 'x', 'merge_to' => '.'
# out :  { 'x' => { 'a' => 1, 'b' => 2 }, 'a' => 1, 'b' => 2 },

access to ‘previous versions’ with ~ and ~~

Before a filter is applied, a copy of the hash to filter is placed under the ‘~’ key in the hash itself.

this filter will at first set the field x to 0, and then reset it to its original value :

filter :in => [
  { :field => 'x', :set => 0 },
  { :field => 'x', :copy_from => '~.x' }
]

For the ‘filter’ expression, ‘~~’ contains the same thing as ‘~’, but for the :filter attribute, it contains the hash (workitem fields) as it was when the expression with the :filter attribute got reached (applied).

‘restore’ and ‘restore_from’

Since these two filter operations leverage ‘~~’, they’re not very useful for the ‘filter’ expression. But they make lots of sense for the :filter attribute.

# in :   { 'x' => 'a', 'y' => 'a' },
filter :in => [
  { 'field' => 'x', 'set' => 'X' },
  { 'field' => 'y', 'set' => 'Y' },
  { 'field' => '/^.$/', 'restore' => true } ]
# out :   { 'x' => 'a', 'y' => 'a' },

# in :   { 'x' => 'a', 'y' => 'a' },
filter :in => [
  { 'field' => 'A', 'set' => {} },
  { 'field' => '.', 'merge_to' => 'A' },
  { 'field' => 'x', 'set' => 'X' },
  { 'field' => 'y', 'set' => 'Y' },
  { 'field' => '/^[a-z]$/', 'restore_from' => 'A' },
  { 'field' => 'A', 'delete' => true } ]
# out :  { 'x' => 'a', 'y' => 'a' })

short forms

Could help make filters a bit more compact.

  • ‘size’, ‘sz’

  • ‘empty’, ‘e’

  • ‘in’, ‘i’

  • ‘has’, ‘h’

  • ‘type’, ‘t’

  • ‘match’, ‘m’

  • ‘smatch’, ‘sm’

  • ‘valid’, ‘v’

  • ‘remove’, ‘rm’, ‘delete’, ‘del’

  • ‘set’, ‘s’

  • ‘copy_to’, ‘cp_to’

  • ‘move_to’, ‘mv_to’

  • ‘merge_to’, ‘mg_to’

  • ‘migrate_to’, ‘mi_to’

  • ‘restore’, ‘restore_from’, ‘rs’

top-level ‘or’

Filters may be used to transform hashes or to validate them. In both cases the filters seen until now were like chained by a big AND.

It’s OK to write

filter :in => [
  { 'field' => 'server_href', 'smatch' => '^https?:\/\/' },
  'or',
  { 'field' => 'nickname', 'type' => 'string' } ]

Granted, this is mostly for validation purposes, but it also works with transformations (as soon as an ‘or’ child succeeds it’s returned and the other children are not evaluated).

compared to the :filter attribute

The :filter attribute accepts participant names, but for this filter expression, it makes no sense accepting partipants… Simply invoke the participant as usual.

The ‘restore’ operation makes lots of sense for the :filter attribute though.

filtering with rules in a block

This filter

filter :in => [
  { :field => 'x', :type => 'string' },
  { :field => 'y', :type => 'number' }
]

can be rewritten as

filter do
  field 'x', :type => 'string'
  field 'y', :type => 'number'
end

The field names can be passed directly as head of each rule :

filter do
  x :type => 'string'
  y :type => 'number'
end

Constant Summary

Constants inherited from FlowExpression

Ruote::Exp::FlowExpression::COMMON_ATT_KEYS

Instance Attribute Summary

Attributes inherited from FlowExpression

#context, #error, #h

Instance Method Summary collapse

Methods inherited from FlowExpression

#ancestor?, #att, #attribute, #attribute_text, #attributes, #cancel, #compile_atts, #compile_variables, do_action, #do_apply, #do_cancel, #do_fail, #do_pause, #do_persist, #do_reply, #do_resume, #do_unpersist, #expand_atts, #fei, fetch, from_h, #handle_on_error, #has_attribute, #initial_persist, #initialize, #iterative_var_lookup, #launch_sub, #lookup_on_error, #lookup_val, #lookup_val_prefix, #lookup_variable, #name, names, #parent, #parent_id, #persist_or_raise, #reply_to_parent, #set_variable, #to_h, #tree, #tree_children, #try_persist, #try_unpersist, #unpersist_or_raise, #unset_variable, #update_tree, #variables

Methods included from WithMeta

#class_def, included

Methods included from WithH

included

Constructor Details

This class inherits a constructor from Ruote::Exp::FlowExpression

Instance Method Details

#applyObject



500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
# File 'lib/ruote/exp/fe_filter.rb', line 500

def apply

  filter =
    referenced_filter || complete_filter || one_line_filter || block_filter

  record = filter.first.delete('record') rescue nil
  flush = filter.first.delete('flush') rescue nil

  record = '__validation_errors__' if record == true

  opts = {
    :double_tilde => parent_id ?
      (parent.h.applied_workitem['fields'] rescue nil) : nil,
    :no_raise => record
  }
    #
    # parent_fields are placed in the ^^ available to the filter

  fields = Ruote.filter(filter, h.applied_workitem['fields'], opts)

  if record and fields.is_a?(Array)
    #
    # validation failed, :record requested, list deviations in
    # the given field name

    (flush ?
      h.applied_workitem['fields'][record] = [] :
      h.applied_workitem['fields'][record] ||= []
    ).concat(fields)

    reply_to_parent(h.applied_workitem)

  else
    #
    # filtering successful

    reply_to_parent(h.applied_workitem.merge('fields' => fields))
  end
end

#reply(workitem) ⇒ Object



540
541
542
543
# File 'lib/ruote/exp/fe_filter.rb', line 540

def reply(workitem)

  # never called
end