Class: Jinx::MatchVisitor

Inherits:
ReferenceVisitor show all
Defined in:
lib/jinx/resource/match_visitor.rb

Overview

A MatchVisitor visits two domain objects’ visitable attributes transitive closure in lock-step.

Direct Known Subclasses

MergeVisitor

Defined Under Namespace

Classes: DefaultMatcher

Constant Summary collapse

DEF_MATCHER =
DefaultMatcher.new

Instance Attribute Summary collapse

Attributes inherited from Visitor

#lineage, #options, #visited

Instance Method Summary collapse

Methods inherited from ReferenceVisitor

#attributes_to_visit

Methods inherited from Visitor

#clear, #current, #cyclic_nodes, #depth_first?, #filter, #from, #node_children, #root, #sync, #to_enum, #visit_children, #visit_node_and_children, #visit_recursive, #visit_root, #visited?

Constructor Details

#initialize(opts = nil) {|obj| ... } ⇒ MatchVisitor

Creates a new visitor which matches source and target domain object references. The domain attributes to visit are determined by calling the selector block given to this initializer. The selector arguments consist of the match source and target.

Parameters:

  • opts (Hash) (defaults to: nil)

    a customizable set of options

  • opts (Symbol, {Symbol => Object}) (defaults to: nil)

    the visit options

Options Hash (opts):

  • :mergeable (Proc)

    the block which determines which attributes are merged

  • :matchable (Proc)

    the block which determines which attributes to match (default is the visit selector)

  • :matcher (:match)

    an object which matches sources to targets

  • :copier (Proc)

    the block which copies an unmatched source

Yields:

  • (obj)

    returns the AttributeEnumerator of attributes to visit next from the current domain object

Yield Parameters:

  • source (Resource)

    the matched source object

Raises:

  • (ArgumentError)


22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/jinx/resource/match_visitor.rb', line 22

def initialize(opts=nil)
  raise ArgumentError.new("Reference visitor missing domain reference selector") unless block_given?
  opts = Options.to_hash(opts)
  @matcher = opts.delete(:matcher) || DEF_MATCHER
  @matchable = opts.delete(:matchable)
  @copier = opts.delete(:copier)
  # the source => target matches
  @matches = {}
  # Apply a filter to the visited reference so that only a matched reference is visited.
  # the reference filter
  flt = opts[:filter]
  opts[:filter] = Proc.new do |src|
    (flt.nil? or flt.call(src)) and !!@matches[src]
  end
  # the class => {id => target} hash
  @id_mtchs = LazyHash.new { Hash.new }
  # Match the source references before navigating from the source to its references, since
  # only a matched reference is visited.
  super do |src|
    tgt = @matches[src]
    # the attributes to match on
    mas = yield(src)
    # match the attribute references
    match_references(src, tgt, mas)
    mas
  end
end

Instance Attribute Details

#matches{Resource => Resource} (readonly)

Returns the domain object matches.

Returns:



8
9
10
# File 'lib/jinx/resource/match_visitor.rb', line 8

def matches
  @matches
end

Instance Method Details

#add_match(source, target) ⇒ Object (private)



164
165
166
167
168
# File 'lib/jinx/resource/match_visitor.rb', line 164

def add_match(source, target)
  @matches[source] = target
  @id_mtchs[source.class][source.identifier] = target if source.identifier
  target
end

#copy_unmatched(source) ⇒ Resource? (private)

Returns a copy of the given source if this ReferenceVisitor has a copier, nil otherwise.

Returns:

  • (Resource, nil)

    a copy of the given source if this ReferenceVisitor has a copier, nil otherwise



178
179
180
181
182
183
# File 'lib/jinx/resource/match_visitor.rb', line 178

def copy_unmatched(source)
  return unless @copier
  copy = @copier.call(source)
  logger.debug { "#{qp} copied unmatched #{source} to #{copy}." } if @verbose
  add_match(source, copy)
end

#identifier_match(source) ⇒ Object (private)

Returns the target matching the given source on the identifier, if any.

Returns:

  • the target matching the given source on the identifier, if any



171
172
173
174
# File 'lib/jinx/resource/match_visitor.rb', line 171

def identifier_match(source)
  tgt = @id_mtchs[source.class][source.identifier] if source.identifier
  @matches[source] = tgt if tgt
end

#match_for(source) ⇒ Object (private)

Returns the target matching the given source.

Returns:

  • the target matching the given source



160
161
162
# File 'lib/jinx/resource/match_visitor.rb', line 160

def match_for(source)
  @matches[source] or identifier_match(source)
end

#match_for_visited(source) ⇒ <Resource> (private)

Returns the source match.

Returns:

Raises:



105
106
107
108
109
# File 'lib/jinx/resource/match_visitor.rb', line 105

def match_for_visited(source)
  target = @matches[source]
  if target.nil? then raise ValidationError.new("Match visitor target not found for #{source}") end
  target
end

#match_reference(source, target, attribute) ⇒ {Resource => Resource} (private)

Matches the given source and target attribute references. The match is performed by this visitor’s matcher.

Parameters:

  • attribute (Symbol)

    the parent reference attribute

  • source (Resource)

    the match visit source

  • target (Resource)

    the match visit target

Returns:



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
# File 'lib/jinx/resource/match_visitor.rb', line 131

def match_reference(source, target, attribute)
  srcs = source.send(attribute).to_enum
  tgts = target.send(attribute).to_enum
  
  # the match targets
  mtchd_tgts = Set.new
  # capture the matched targets and the the unmatched sources
  unmtchd_srcs = srcs.reject do |src|
    # the prior match, if any
    tgt = match_for(src)
    mtchd_tgts << tgt if tgt
  end
  
  # the unmatched targets
  unmtchd_tgts = tgts.difference(mtchd_tgts)
  logger.debug { "#{qp} matching #{unmtchd_tgts.qp}..." } if @verbose and not unmtchd_tgts.empty?
  # match the residual targets and sources
  rsd_mtchs = @matcher.match(unmtchd_srcs, unmtchd_tgts, source, attribute)
  # add residual matches
  rsd_mtchs.each { |src, tgt| add_match(src, tgt) }
  logger.debug { "#{qp} matched #{rsd_mtchs.qp}..." } if @verbose and not rsd_mtchs.empty?
  # The source => target match hash.
  # If there is a copier, then copy each unmatched source.
  matches = srcs.to_compact_hash { |src| match_for(src) or copy_unmatched(src) }
  
  matches
end

#match_references(source, target, attributes) ⇒ {Resource => Resource} (private)

Returns the referenced attribute matches.

Parameters:

  • source (Resource)

    (see #match_visited)

  • target (Resource)

    the source match

  • attributes (<Symbol>)

    the attributes to match on

Returns:



115
116
117
118
119
120
121
122
# File 'lib/jinx/resource/match_visitor.rb', line 115

def match_references(source, target, attributes)
  # collect the references to visit
  matches = {}
  attributes.each do |ma|
    matches.merge!(match_reference(source, target, ma))
  end
  matches
end

#visit(source, target) {|target, source| ... } ⇒ Object

Visits the source and target.

If a block is given to this method, then this method returns the evaluation of the block on the visited source reference and its matching copy, if any. The default return value is the target which matches source.

Parameters:

  • source (Resource)

    the match visit source

  • target (Resource)

    the match visit target

Yields:

  • (target, source)

    the optional block to call on the matched source and target

Yield Parameters:

  • source (Resource)

    the visited source domain object

  • target (Resource)

    the domain object which matches the visited source

  • from (Resource)

    the visiting domain object

  • property (Property)

    the visiting property



63
64
65
66
67
68
69
70
71
# File 'lib/jinx/resource/match_visitor.rb', line 63

def visit(source, target, &block)
  # clear the match hashes
  @matches.clear
  @id_mtchs.clear
  # seed the matches with the top-level source => target
  add_match(source, target)
  # Visit the source reference.
  super(source) { |src| visit_matched(src, &block) }
end

#visit_matched(source) {|target, source| ... } ⇒ Object (private)

Visits the given source domain object.

Parameters:

  • source (Resource)

    the match visit source

Yields:

  • (target, source)

    the optional block to call on the matched source and target

Yield Parameters:

  • source (Resource)

    the visited source domain object

  • target (Resource)

    the domain object which matches the visited source

  • from (Resource)

    the visiting domain object

  • property (Property)

    the visiting property



92
93
94
95
96
97
98
99
100
# File 'lib/jinx/resource/match_visitor.rb', line 92

def visit_matched(source)
  tgt = @matches[source] || return
  # Match the unvisited matchable references, if any.
  if @matchable then
    mas = @matchable.call(source) - attributes_to_visit(source)
    mas.each { |ma| match_reference(source, tgt, ma) }
  end
  block_given? ? yield(source, tgt) : tgt
end