jinx 2.1.2 → 2.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/History.md CHANGED
@@ -1,6 +1,10 @@
1
1
  This history lists major release themes. See the GitHub commits (https://github.com/jinx/core)
2
2
  for change details.
3
3
 
4
+ 2.1.3 / 2012-08-18
5
+ ------------------
6
+ * Pull UID out of uniquifier.
7
+
4
8
  2.1.2 / 2012-06-12
5
9
  ------------------
6
10
  * Fine-tune introspection.
data/LEGAL CHANGED
@@ -1,5 +1,4 @@
1
1
  LEGAL NOTICE INFORMATION
2
2
  ------------------------
3
-
4
3
  All the files in this distribution are covered under either the MIT
5
4
  license (see the file LICENSE).
data/README.md CHANGED
@@ -1,6 +1,5 @@
1
1
  JINX: JRuby Java introspector
2
2
  =============================
3
-
4
3
  **Home**: [http://github.com/jinx/core](http://github.com/jinx/core)
5
4
  **Git**: [http://github.com/jinx/core](http://github.com/jinx/core)
6
5
  **Author**: OHSU Knight Cancer Institute
@@ -9,12 +8,10 @@ JINX: JRuby Java introspector
9
8
 
10
9
  Synopsis
11
10
  --------
12
-
13
11
  Jinx introspects a Java application domain package.
14
12
 
15
13
  Feature List
16
14
  ------------
17
-
18
15
  1. Introspects Java properties.
19
16
 
20
17
  2. Annotates the introspected object model with additional meta-data.
@@ -39,6 +36,5 @@ See the [example](http://github.com/jinx/core/tree/master/examples/family) for a
39
36
 
40
37
  Copyright
41
38
  ---------
42
-
43
39
  Jinx © 2012 by [Oregon Health & Science University](http://www.ohsu.edu/xd/health/services/cancer/index.cfm).
44
40
  Jinx is licensed under the MIT license. Please see the LICENSE and LEGAL files for more information.
@@ -66,7 +66,9 @@ module Jinx
66
66
  # @option opts [Boolean] :debug whether to include debug messages in the log file
67
67
  # @return [MultilineLogger] the global logger
68
68
  def open(dev=nil, opts=nil)
69
- raise RuntimeError.new("Log already open") if open?
69
+ if open? then
70
+ raise RuntimeError.new("The logger has already opened the log#{' file ' + @dev if String === @dev}")
71
+ end
70
72
  dev, opts = nil, dev if Hash === dev
71
73
  dev ||= default_log_file(Options.get(:app, opts))
72
74
  FileUtils.mkdir_p(File.dirname(dev)) if String === dev
@@ -0,0 +1,39 @@
1
+ require 'jinx/helpers/uid'
2
+
3
+ module Jinx
4
+ # A mix-in to convert a String to a unique value.
5
+ module StringUniquifier
6
+ # Returns a relatively unique String for the given base String. Each call returns
7
+ # a distinct value.
8
+ #
9
+ # This method is useful to transform a String object key to a unique value for
10
+ # testing purposes.
11
+ #
12
+ # The unique value is comprised of a prefix and suffix. The prefix is the base value
13
+ # with spaces replaced by an underscore. The suffix is given by a {Jinx::UID.generate}
14
+ # qualifier converted to digits and lower-case letters, excluding the digits 0, 1 and
15
+ # the characters l, o to avoid confusion.
16
+ #
17
+ # @example
18
+ # Jinx::StringUniquifier.uniquify('Groucho') #=> Groucho_wiafye6e
19
+ #
20
+ # @param value [String] the value to make unique
21
+ # @return [String, nil] the new unique value
22
+ # @raise [ArgumentError] if value is not a String
23
+ def self.uniquify(value)
24
+ raise ArgumentError.new("#{value.qp} is not a String") unless String === value
25
+ s = ''
26
+ n = UID.generate
27
+ while n > 0 do
28
+ n, m = n.divmod(32)
29
+ s << CHARS[m]
30
+ end
31
+ [value.gsub(' ', '_'), s].join('_')
32
+ end
33
+
34
+ private
35
+
36
+ # The qualifier character range.
37
+ CHARS = 'abcdefghijkmnpqrstuvwxyz23456789'
38
+ end
39
+ end
@@ -22,8 +22,6 @@ class Object
22
22
  # @children = []
23
23
  # parent.children << self if parent
24
24
  # end
25
- #
26
- # def to_s;
27
25
  # end
28
26
  # a = Node.new('a'); b = Node.new('b', a), c = Node.new('c', a); d = Node.new('d', c)
29
27
  # a.transitive_closure { |node| node.children }.to_a.join(", ") #=> a, b, c, d
@@ -0,0 +1,19 @@
1
+ module Jinx
2
+ # A unique identifier generator.
3
+ module UID
4
+ # Returns a relatively unique integer. Successive calls to this method
5
+ # within the same time zone spaced more than a millisecond apart return different
6
+ # integers. Each generated qualifier is greater than the previous by an unspecified
7
+ # amount.
8
+ def self.generate
9
+ # the first date that this method could be called
10
+ @first ||= Date.new(2011, 12, 01)
11
+ # days as integer + milliseconds as fraction since the first date
12
+ diff = DateTime.now - @first
13
+ # shift a tenth of a milli up into the integer portion
14
+ decimillis = diff * 24 * 60 * 60 * 10000
15
+ # truncate the fraction
16
+ decimillis.truncate
17
+ end
18
+ end
19
+ end
@@ -193,7 +193,7 @@ module Jinx
193
193
  # @return [Visitor] the filter visitor
194
194
  # @yield [parent, children] the filter to select which of the children to visit next
195
195
  # @yieldparam parent the currently visited node
196
- # @yieldparam children the nodes slated by this Visitor to visit next
196
+ # @yieldparam [Array] children the nodes slated by this visitor to visit next
197
197
  # @raise [ArgumentError] if a block is not given to this method
198
198
  def filter
199
199
  raise ArgumentError.new("A filter block is not given to the visitor filter method") unless block_given?
@@ -229,20 +229,28 @@ module Jinx
229
229
  # Visits the root node and all descendants.
230
230
  def visit_root(node, &operator)
231
231
  clear
232
- prune_cycle_nodes(node) if @prune_cycle_flag
233
- # visit the root node
234
- visit_recursive(node, &operator)
232
+ # Exclude cycles if the prune cycles flag is set.
233
+ @exclude.merge!(cyclic_nodes(node)) if @prune_cycle_flag
234
+ # Visit the root node.
235
+ result = visit_recursive(node, &operator)
236
+ # Reset the exclusions if the prune cycles flag is set.
237
+ @exclude.clear if @prune_cycle_flag
238
+ result
235
239
  end
236
-
237
- # Excludes the internal nodes in cycles starting and ending at the given root.
238
- def prune_cycle_nodes(root)
239
- @exclude.clear
240
- # visit the root, which will detect cycles, and remove the visited nodes afterwords
241
- @prune_cycle_flag = false
242
- to_enum(root).collect.each { |node| @visited.delete(node) }
243
- @prune_cycle_flag = true
244
- # add each cyclic internal node to the exclude list
245
- @cycles.each { |cycle| cycle[1...-1].each { |node| @exclude << node } if cycle.first == root }
240
+
241
+ # @example
242
+ # visitor.visit(a)
243
+ # visit.to_enum #=> [a, b, c, u, v, x, y, z]
244
+ # visit.cycles #=> [[a, b, u, x, a], [c, z, c]]
245
+ # visit.cyclic_nodes #=> [b, u, x, c, z]
246
+ # @return [Array] the non-root nodes in visit cycles
247
+ def cyclic_nodes(root)
248
+ copts = @options.reject { |k, v| k == :prune_cycle }
249
+ cycler = Visitor.new(copts, &@navigator)
250
+ cycler.visit(root)
251
+ cyclic = cycler.cycles.flatten.uniq
252
+ cyclic.delete(root)
253
+ cyclic
246
254
  end
247
255
 
248
256
  def visit_recursive(node, &operator)
@@ -298,7 +306,9 @@ module Jinx
298
306
  @visitor = visitor
299
307
  @root = node
300
308
  end
301
-
309
+
310
+ # @yield [node] operates on the visited node
311
+ # @yieldparam node the visited node
302
312
  def each
303
313
  @visitor.visit(@root) { |node| yield(node) }
304
314
  end
@@ -314,8 +324,8 @@ module Jinx
314
324
 
315
325
  # Visits the given pair of nodes.
316
326
  #
317
- # Raises ArgumentError if nodes does not consist of either two node arguments or one two-item Array
318
- # argument.
327
+ # @param [(Object, Object), <(Object, Object)>] nodes the node pair
328
+ # @raise [ArgumentError] if the arguments do not consist of either two nodes or one two-item array
319
329
  def visit(*nodes)
320
330
  if nodes.size == 1 then
321
331
  nodes = nodes.first
@@ -324,10 +334,10 @@ module Jinx
324
334
  super(nodes)
325
335
  end
326
336
 
327
- # Returns an Enumerable which applies the given block to each matched node starting at the given nodes.
328
- #
329
- # Raises ArgumentError if nodes does not consist of either two node arguments or one two-item Array
330
- # argument.
337
+ # @param (see #visit)
338
+ # @param [(Object, Object), <(Object, Object)>] nodes
339
+ # @return [Enumerable] the result of applying the given block to each matched node starting at the given root nodes
340
+ # @raise [ArgumentError] if the arguments do not consist of either two nodes or one two-item array
331
341
  def to_enum(*nodes)
332
342
  if nodes.size == 1 then
333
343
  nodes = nodes.first
@@ -17,9 +17,9 @@ module Jinx
17
17
  # Adds an optional {attribute=>value} constructor parameter to this class.
18
18
  def add_attribute_value_initializer
19
19
  class << self
20
- def new(opts=nil)
20
+ def new(params=nil)
21
21
  obj = super()
22
- obj.merge_attributes(opts) if opts
22
+ obj.merge_attributes(params) if params
23
23
  obj
24
24
  end
25
25
  end
@@ -246,7 +246,7 @@ module Jinx
246
246
  @local_prop_hash = {}
247
247
  @prop_hash = append_ancestor_enum(@local_prop_hash) { |par| par.property_hash }
248
248
  @attributes = Enumerable::Enumerator.new(@prop_hash, :each_key)
249
- @local_mndty_flt = Set.new
249
+ @local_mndty_attrs = Set.new
250
250
  @local_defaults = {}
251
251
  @defaults = append_ancestor_enum(@local_defaults) { |par| par.defaults }
252
252
  end
@@ -393,7 +393,7 @@ module Jinx
393
393
 
394
394
  # @param [Symbol] attribute the mandatory attribute
395
395
  def add_mandatory_attribute(attribute)
396
- @local_mndty_flt << standard_attribute(attribute)
396
+ @local_mndty_attrs << standard_attribute(attribute)
397
397
  end
398
398
 
399
399
  # Marks the given attribute with flags supported by {Property#qualify}.
@@ -414,16 +414,16 @@ module Jinx
414
414
  # An attribute declared in a superclass Resource is hidden from this Resource but retained in
415
415
  # the declaring Resource.
416
416
  def remove_attribute(attribute)
417
- std_prop = standard_attribute(attribute)
417
+ sa = standard_attribute(attribute)
418
418
  # if the attribute is local, then delete it, otherwise filter out the superclass attribute
419
- prop = @local_prop_hash.delete(std_prop)
420
- if prop then
419
+ sp = @local_prop_hash.delete(sa)
420
+ if sp then
421
421
  # clear the inverse, if any
422
- clear_inverse(prop)
422
+ clear_inverse(sp)
423
423
  # remove from the mandatory attributes, if necessary
424
- @local_mndty_flt.delete(std_prop)
424
+ @local_mndty_attrs.delete(sa)
425
425
  # remove from the attribute => metadata hash
426
- @local_std_prop_hash.delete_if { |aliaz, pa| pa == std_prop }
426
+ @local_std_prop_hash.delete_if { |aliaz, pa| pa == sa }
427
427
  else
428
428
  # Filter the superclass hashes.
429
429
  anc_prop_hash = @prop_hash.components[1]
@@ -478,8 +478,8 @@ module Jinx
478
478
  #
479
479
  # @see #mandatory_attributes
480
480
  def collect_mandatory_attributes
481
- @local_mndty_flt.merge!(default_mandatory_local_attributes)
482
- append_ancestor_enum(@local_mndty_flt) { |par| par.mandatory_attributes }
481
+ @local_mndty_attrs.merge!(default_mandatory_local_attributes)
482
+ append_ancestor_enum(@local_mndty_attrs) { |par| par.mandatory_attributes }
483
483
  end
484
484
 
485
485
  def default_mandatory_local_attributes
@@ -77,15 +77,6 @@ module Jinx
77
77
  @inv_prop.attribute if @inv_prop
78
78
  end
79
79
 
80
- # An attribute is unidirectional if both of the following is true:
81
- # * there is no distinct {#inverse} attribute
82
- # * the attribute is not a {#dependent?} with more than one owner
83
- #
84
- # @return [Boolean] whether this attribute does not have an inverse
85
- def unidirectional?
86
- inverse.nil? and not (dependent? and type.owner_attributes.size > 1)
87
- end
88
-
89
80
  # @param [Class] the attribute return type
90
81
  def type=(klass)
91
82
  return if klass == @type
@@ -135,11 +126,6 @@ module Jinx
135
126
  @inv_prop.qualify(:disjoint) if disjoint?
136
127
  logger.debug { "Assigned #{@declarer.qp}.#{self} attribute inverse to #{type.qp}.#{attribute}." }
137
128
  end
138
-
139
- # @return [Boolean] whether this property has an inverse
140
- def bidirectional?
141
- !!@inv_prop
142
- end
143
129
 
144
130
  # @return [Property, nil] the property for the {#inverse} attribute, if any
145
131
  def inverse_property
@@ -156,91 +142,10 @@ module Jinx
156
142
  if @restrictions then @restrictions.each { |prop| prop.qualify(*flags) } end
157
143
  end
158
144
 
159
- # @return [Boolean] whether the subject attribute encapsulates a Java attribute
160
- def java_property?
161
- JavaProperty === self
162
- end
163
-
164
- # @return [Boolean] whether the subject attribute returns a domain object or collection of domain objects
165
- def domain?
166
- # the type must be a Ruby class rather than a Java Class, and include the Domain mix-in
167
- Class === type and type < Resource
168
- end
169
-
170
- # @return [Boolean] whether the subject attribute is not a domain object attribute
171
- def nondomain?
172
- not domain?
173
- end
174
-
175
- # @return [Boolean] whether the subject attribute return type is a collection
176
- def collection?
177
- @flags.include?(:collection)
178
- end
179
-
180
- # Returns whether the subject attribute is a dependent on a parent. See the Jinx configuration
181
- # documentation for a dependency description.
182
- #
183
- # @return [Boolean] whether the attribute references a dependent
184
- def dependent?
185
- @flags.include?(:dependent)
186
- end
187
-
188
- # Returns whether the subject attribute must have a value when it is saved
189
- #
190
- # @return [Boolean] whether the attribute is mandatory
191
- def mandatory?
192
- @declarer.mandatory_attributes.include?(attribute)
193
- end
194
-
195
- # An attribute is derived if the attribute value is set by setting another attribute, e.g. if this
196
- # attribute is the inverse of a dependent owner attribute.
197
- #
198
- # @return [Boolean] whether this attribute is derived from another attribute
199
- def derived?
200
- dependent? and !!inverse
201
- end
202
-
203
145
  # @return [Boolean] this attribute's inverse attribute if the inverse is a derived attribute, or nil otherwise
204
146
  def derived_inverse
205
147
  @inv_prop.attribute if @inv_prop and @inv_prop.derived?
206
148
  end
207
-
208
- # An independent attribute is a reference to one or more non-dependent Resource objects.
209
- # An {#owner?} attribute is independent.
210
- #
211
- # @return [Boolean] whether the subject attribute is a non-dependent domain attribute
212
- def independent?
213
- domain? and not dependent?
214
- end
215
-
216
- # @return [Boolean] whether this attribute is a collection with a collection inverse
217
- def many_to_many?
218
- return false unless collection?
219
- inv_prop = inverse_property
220
- inv_prop and inv_prop.collection?
221
- end
222
-
223
- # @return [Boolean] whether the subject attribute is a dependency owner
224
- def owner?
225
- @flags.include?(:owner)
226
- end
227
-
228
- # @return [Boolean] whether this is a dependent attribute which has exactly one owner value
229
- # chosen from several owner attributes
230
- def disjoint?
231
- @flags.include?(:disjoint)
232
- end
233
-
234
- # @return [Boolean] whether this attribute is a dependent which does not have a Java
235
- # inverse owner attribute
236
- def unidirectional_java_dependent?
237
- dependent? and java_property? and not bidirectional_java_association?
238
- end
239
-
240
- # @return [Boolean] whether this is a Java attribute which has a Java inverse
241
- def bidirectional_java_association?
242
- inverse and java_property? and inverse_property.java_property?
243
- end
244
149
 
245
150
  # Creates a new declarer attribute which restricts this attribute.
246
151
  # This method should only be called by a {Resource} class, since the class is responsible
@@ -27,7 +27,8 @@ module Jinx
27
27
  JavaProperty === self
28
28
  end
29
29
 
30
- # @return [Boolean] whether the subject attribute returns a domain object or collection of domain objects
30
+ # @return [Boolean] whether the subject attribute returns a domain object or a collection
31
+ # of domain objects
31
32
  def domain?
32
33
  # the type must be a Ruby class rather than a Java Class, and include the Domain mix-in
33
34
  Class === type and type < Resource
@@ -27,8 +27,12 @@ module Jinx
27
27
  @copier = opts.delete(:copier)
28
28
  # the source => target matches
29
29
  @matches = {}
30
- # Apply a filter to the selected references so that only a matched reference is visited.
31
- opts[:filter] = Proc.new { |src| @matches[src] }
30
+ # Apply a filter to the visited reference so that only a matched reference is visited.
31
+ # the reference filter
32
+ flt = opts[:filter]
33
+ opts[:filter] = Proc.new do |src|
34
+ (flt.nil? or flt.call(src)) and !!@matches[src]
35
+ end
32
36
  # the class => {id => target} hash
33
37
  @id_mtchs = LazyHash.new { Hash.new }
34
38
  # Match the source references before navigating from the source to its references, since
@@ -60,7 +60,7 @@ module Jinx
60
60
  # merge the non-domain attributes
61
61
  target.merge_attributes(source)
62
62
  # merge the source domain attributes into the target
63
- target.merge(source, mas, @matches)
63
+ target.merge_attributes(source, mas, @matches, &@filter)
64
64
  end
65
65
  end
66
66
  end
@@ -23,21 +23,19 @@ module Jinx
23
23
  # {Propertied#mergeable_attributes}.
24
24
  #
25
25
  # The merge is performed by calling {#merge_attribute} on each attribute with the matches
26
- # and merger block given to this method.
26
+ # and filter block given to this method.
27
27
  #
28
28
  # @param [Mergeable, {Symbol => Object}] other the source domain object or value hash to merge from
29
29
  # @param [<Symbol>, nil] attributes the attributes to merge (default {Propertied#nondomain_attributes})
30
30
  # @param [{Resource => Resource}, nil] the optional merge source => target reference matches
31
- # @yield [attribute, oldval, newval] the optional merger block
32
- # @yieldparam [Symbol] attribute the merge target attribute
33
- # @yieldparam oldval the current merge attribute value
34
- # @yieldparam newval the new merge attribute value
31
+ # @yield [value] the optional filter block
32
+ # @yieldparam value the source merge attribute value
35
33
  # @return [Mergeable] self
36
34
  # @raise [ArgumentError] if none of the following are true:
37
35
  # * other is a Hash
38
36
  # * attributes is non-nil
39
37
  # * the other class responds to +mergeable_attributes+
40
- def merge_attributes(other, attributes=nil, matches=nil, &merger)
38
+ def merge_attributes(other, attributes=nil, matches=nil, &filter)
41
39
  return self if other.nil? or other.equal?(self)
42
40
  attributes = [attributes] if Symbol === attributes
43
41
  attributes ||= self.class.mergeable_attributes
@@ -45,11 +43,13 @@ module Jinx
45
43
  # if the source object is not a hash, then convert it to an attribute => value hash
46
44
  vh = Hashable === other ? other : other.value_hash(attributes)
47
45
  # merge the value hash
48
- vh.each { |pa, value| merge_attribute(pa, value, matches, &merger) }
46
+ vh.each { |pa, value| merge_attribute(pa, value, matches, &filter) }
49
47
  self
50
48
  end
51
49
 
52
- alias :merge :merge_attributes
50
+ def merge(*args, &filter)
51
+ merge_attributes(*args, &filter)
52
+ end
53
53
 
54
54
  alias :merge! :merge
55
55
 
@@ -77,19 +77,26 @@ module Jinx
77
77
  def merge_attribute(attribute, newval, matches=nil)
78
78
  # the previous value
79
79
  oldval = send(attribute)
80
- # If nothing to merge or a block can take over, then bail.
81
- if newval.nil? or mergeable__equal?(oldval, newval) then
82
- return oldval
83
- elsif block_given? then
84
- return yield(attribute, oldval, value)
80
+ # Filter the newval into the srcval.
81
+ srcval = if newval and block_given? then
82
+ if newval.collection? then
83
+ newval.select { |v| yield(v) }
84
+ elsif yield(newval) then
85
+ newval
86
+ end
87
+ else
88
+ newval
85
89
  end
86
90
 
91
+ # If there is no point in merging, then bail.
92
+ return oldval if srcval.nil_or_empty? or mergeable__equal?(oldval, newval)
93
+
87
94
  # Discriminate between a domain and non-domain attribute.
88
95
  prop = self.class.property(attribute)
89
96
  if prop.domain? then
90
- merge_domain_property_value(prop, oldval, newval, matches)
97
+ merge_domain_property_value(prop, oldval, srcval, matches)
91
98
  else
92
- merge_nondomain_property_value(prop, oldval, newval)
99
+ merge_nondomain_property_value(prop, oldval, srcval)
93
100
  end
94
101
  end
95
102
 
@@ -14,31 +14,31 @@ module Jinx
14
14
  # The required selector block given to this initializer determines which attributes to
15
15
  # visit. The references to visit next thus consist of the current domain object's selector
16
16
  # attributes' values. If the :filter option is set, then the given filter block is applied
17
- # to the selected attribute references to restrict which domain objects will be visited.
17
+ # to each selected attribute reference to determine which domain objects will be visited.
18
18
  #
19
19
  # @param opts (see Visitor#initialize)
20
- # @option opts [Proc] :filter an optional filter on the references to visit
20
+ # @option opts [Proc] :filter an optional filter on the reference to visit
21
21
  # @yield [obj] returns the {AttributeEnumerator} of attributes to visit next from the
22
22
  # current domain object
23
23
  # @yieldparam [Resource] obj the current domain object
24
24
  def initialize(opts=nil, &selector)
25
25
  raise ArgumentError.new("Reference visitor missing domain reference selector") unless block_given?
26
26
  # the property selector
27
- @flt_sel = selector
27
+ @selector = selector
28
28
  # the reference filter
29
- flt = Options.get(:filter, opts)
29
+ @filter = Options.get(:filter, opts)
30
30
  # Initialize the Visitor with a reference enumerator which selects the reference
31
31
  # attributes and applies the optional filter if necessary.
32
32
  @ref_enums = {}
33
- super do |ref|
33
+ super do |obj|
34
34
  # the reference property filter
35
- ras = attributes_to_visit(ref)
35
+ ras = attributes_to_visit(obj)
36
36
  if ras then
37
- logger.debug { "#{qp} visiting #{ref} attributes #{ras.pp_s(:single_line)}..." } if @verbose
37
+ logger.debug { "#{qp} visiting #{obj} attributes #{ras.pp_s(:single_line)}..." } if @verbose
38
38
  # an enumerator on the reference properties
39
- enum = ReferenceEnumerator.new(ref, ras.properties)
39
+ enum = ReferenceEnumerator.new(obj, ras.properties)
40
40
  # If there is a reference filter, then apply it to the enum references.
41
- flt ? enum.filter(&flt) : enum
41
+ @filter ? enum.filter(&@filter) : enum
42
42
  end
43
43
  end
44
44
  end
@@ -48,7 +48,7 @@ module Jinx
48
48
  # @param [Resource] obj the visiting object
49
49
  # @return [Propertied::Filter] the attributes to visit
50
50
  def attributes_to_visit(obj)
51
- @flt_sel.call(obj)
51
+ @selector.call(obj)
52
52
  end
53
53
  end
54
54
  end
@@ -1,32 +1,29 @@
1
- require 'jinx/helpers/uniquifier'
1
+ require 'jinx/resource/uniquifier_cache'
2
2
 
3
3
  module Jinx
4
4
  # The Unique mix-in makes values unique within the scope of a Resource class.
5
5
  module Unique
6
- # Makes the given String value unique in the context of this object's class.
7
- # @return nil if value is nil
8
- # Raises TypeError if value is neither nil nor a String.
9
- def uniquify_value(value)
10
- unless String === value or value.nil? then
11
- raise TypeError.new("Cannot uniquify #{qp} non-String value #{value}")
12
- end
13
- Uniquifier.instance.uniquify(self, value)
14
- end
15
-
16
- # Makes the secondary key unique by replacing each String key attribute value
17
- # with a unique value.
6
+ # Replaces each String secondary and alternate key property with a unique
7
+ # value. Successive calls to this method for domain objects of the same class
8
+ # replace the same String key property values with the same unique value.
9
+ #
10
+ # @return [Resource] self
18
11
  def uniquify
19
12
  uniquify_attributes(self.class.secondary_key_attributes)
20
13
  uniquify_attributes(self.class.alternate_key_attributes)
14
+ self
21
15
  end
22
-
23
- # Makes the given attribute values unique by replacing each String value
24
- # with a unique value.
16
+
17
+ private
18
+
19
+ # Makes this domain object's String values for the given attributes unique.
20
+ #
21
+ # @param [<Symbol>] the key attributes to uniquify
25
22
  def uniquify_attributes(attributes)
26
23
  attributes.each do |ka|
27
24
  oldval = send(ka)
28
25
  next unless String === oldval
29
- newval = uniquify_value(oldval)
26
+ newval = UniquifierCache.instance.get(self, oldval)
30
27
  set_property_value(ka, newval)
31
28
  logger.debug { "Reset #{qp} #{ka} from #{oldval} to unique value #{newval}." }
32
29
  end
@@ -0,0 +1,34 @@
1
+ require 'singleton'
2
+ require 'jinx/helpers/lazy_hash'
3
+ require 'jinx/helpers/string_uniquifier'
4
+
5
+ module Jinx
6
+ # A utility class to cache key value qualifiers.
7
+ class UniquifierCache
8
+ include Singleton
9
+
10
+ def initialize
11
+ @cache = Jinx::LazyHash.new { Hash.new }
12
+ end
13
+
14
+ # Returns the unique value generated for the given object and value.
15
+ # Successive calls to this method for domain objects of the same class
16
+ # and a given value return the same result.
17
+ #
18
+ # @example
19
+ # Jinx::UniquifierCache.instance.get(person, 'Groucho') #=> Groucho_wy874e6e
20
+ # Jinx::UniquifierCache.instance.get(person.copy, 'Groucho') #=> Groucho_wy87ye6e
21
+ #
22
+ # @param [Resource] obj the domain object containing the value
23
+ # @param [String] value the value to make unique
24
+ # @return [String] the unique value
25
+ def get(obj, value)
26
+ @cache[obj.class][value] ||= StringUniquifier.uniquify(value)
27
+ end
28
+
29
+ # Clears all cache entries.
30
+ def clear
31
+ @cache.clear
32
+ end
33
+ end
34
+ end
@@ -1,3 +1,3 @@
1
1
  module Jinx
2
- VERSION = '2.1.2'
2
+ VERSION = '2.1.3'
3
3
  end
@@ -0,0 +1,11 @@
1
+ require File.dirname(__FILE__) + '/../../../helper'
2
+ require 'test/unit'
3
+ require 'jinx/helpers/string_uniquifier'
4
+
5
+ class StringUniquifierTest < Test::Unit::TestCase
6
+ def test_uniquify
7
+ u1 = Jinx::StringUniquifier.uniquify('Groucho')
8
+ u2 = Jinx::StringUniquifier.uniquify('Groucho')
9
+ assert_not_equal(u1, u2, "Consecutive uniquifier calls not unique")
10
+ end
11
+ end
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: jinx
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 2.1.2
5
+ version: 2.1.3
6
6
  platform: ruby
7
7
  authors:
8
8
  - OHSU
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2012-06-12 00:00:00 Z
13
+ date: 2012-07-17 00:00:00 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: bundler
@@ -142,9 +142,10 @@ files:
142
142
  - lib/jinx/helpers/pretty_print.rb
143
143
  - lib/jinx/helpers/set.rb
144
144
  - lib/jinx/helpers/stopwatch.rb
145
+ - lib/jinx/helpers/string_uniquifier.rb
145
146
  - lib/jinx/helpers/transformer.rb
146
147
  - lib/jinx/helpers/transitive_closure.rb
147
- - lib/jinx/helpers/uniquifier.rb
148
+ - lib/jinx/helpers/uid.rb
148
149
  - lib/jinx/helpers/validation.rb
149
150
  - lib/jinx/helpers/visitor.rb
150
151
  - lib/jinx/import/class_path_modifier.rb
@@ -171,6 +172,7 @@ files:
171
172
  - lib/jinx/resource/reference_path_visitor.rb
172
173
  - lib/jinx/resource/reference_visitor.rb
173
174
  - lib/jinx/resource/unique.rb
175
+ - lib/jinx/resource/uniquifier_cache.rb
174
176
  - lib/jinx/version.rb
175
177
  - spec/defaults_spec.rb
176
178
  - spec/definitions/model/alias/child.rb
@@ -212,8 +214,8 @@ files:
212
214
  - test/lib/jinx/helpers/partial_order_test.rb
213
215
  - test/lib/jinx/helpers/pretty_print_test.rb
214
216
  - test/lib/jinx/helpers/stopwatch_test.rb
217
+ - test/lib/jinx/helpers/string_uniquifier_test.rb
215
218
  - test/lib/jinx/helpers/transitive_closure_test.rb
216
- - test/lib/jinx/helpers/uniquifier_test.rb
217
219
  - test/lib/jinx/helpers/visitor_test.rb
218
220
  - test/lib/jinx/import/java_test.rb
219
221
  - test/lib/jinx/import/mixed_case_test.rb
@@ -265,8 +267,8 @@ test_files:
265
267
  - test/lib/jinx/helpers/partial_order_test.rb
266
268
  - test/lib/jinx/helpers/pretty_print_test.rb
267
269
  - test/lib/jinx/helpers/stopwatch_test.rb
270
+ - test/lib/jinx/helpers/string_uniquifier_test.rb
268
271
  - test/lib/jinx/helpers/transitive_closure_test.rb
269
- - test/lib/jinx/helpers/uniquifier_test.rb
270
272
  - test/lib/jinx/helpers/visitor_test.rb
271
273
  - test/lib/jinx/import/java_test.rb
272
274
  - test/lib/jinx/import/mixed_case_test.rb
@@ -1,83 +0,0 @@
1
- require 'singleton'
2
- require 'jinx/helpers/lazy_hash'
3
-
4
- module Jinx
5
- # A utility class to generate value qualifiers.
6
- class Uniquifier
7
- include Singleton
8
-
9
- # Returns a relatively unique integral qualifier. Successive calls to this method
10
- # within the same time zone spaced more than a millisecond apart return different
11
- # integers. Each generated qualifier is greater than the previous by an unspecified
12
- # amount.
13
- def self.qualifier
14
- # the first date that this method could be called
15
- @first ||= Date.new(2011, 12, 01)
16
- # days as integer + milliseconds as fraction since the first date
17
- diff = DateTime.now - @first
18
- # shift a tenth of a milli up into the integer portion
19
- decimillis = diff * 24 * 60 * 60 * 10000
20
- # truncate the fraction
21
- decimillis.truncate
22
- end
23
-
24
- def initialize
25
- @cache = Jinx::LazyHash.new { Hash.new }
26
- end
27
-
28
- # Returns a relatively unique String for the given base String object or
29
- # (object, String value) pair. In the former case, each call returns a distinct value.
30
- # In the latter case, successive calls of the same String value for the same object
31
- # class return the same unique value.
32
- #
33
- # This method is useful to transform a String object key to a unique value for testing
34
- # purposes.
35
- #
36
- # The unique value is comprised of a prefix and suffix. The prefix is the base value
37
- # with spaces replaced by an underscore. The suffix is a {Jinx::Uniquifier.qualifier}
38
- # converted to digits and lower-case letters, excluding the digits 0, 1 and characters
39
- # l, o to avoid confusion.
40
- #
41
- # @example
42
- # Jinx::Uniquifier.instance.uniquify('Groucho') #=> Groucho_wiafye6e
43
- # Jinx::Uniquifier.instance.uniquify('Groucho') #=> Groucho_uqafye6e
44
- # Jinx::Uniquifier.instance.uniquify('Groucho Marx') #=> Groucho_ay23ye6e
45
- # Jinx::Uniquifier.instance.uniquify(person, 'Groucho') #=> Groucho_wy874e6e
46
- # Jinx::Uniquifier.instance.uniquify(person, 'Groucho') #=> Groucho_wy87ye6e
47
- #
48
- # @param obj the object containing the value to uniquify
49
- # @param value [String, nil] the value to make unique, or nil if the containing object is a String
50
- # @return [String, nil] the new unique value, or nil if the containing object is a String
51
- # and the given value is nil
52
- def uniquify(obj, value=nil)
53
- if String === obj then
54
- to_unique(obj)
55
- elsif value then
56
- @cache[obj.class][value] ||= to_unique(value)
57
- end
58
- end
59
-
60
- def clear
61
- @cache.clear
62
- end
63
-
64
- private
65
-
66
- CHARS = 'abcdefghijkmnpqrstuvwxyz23456789'
67
-
68
- # @param value [String, nil] the value to make unique
69
- # @return [String] the new unique value, or nil if the given value is nil
70
- # @raise [ArgumentError] if value is neither a String nor nil
71
- def to_unique(value)
72
- return if value.nil?
73
- raise ArgumentError.new("#{value.qp} is not a String") unless String === value
74
- s = ''
75
- n = Jinx::Uniquifier.qualifier
76
- while n > 0 do
77
- n, m = n.divmod(32)
78
- s << CHARS[m]
79
- end
80
- [value.gsub(' ', '_'), s].join('_')
81
- end
82
- end
83
- end
@@ -1,11 +0,0 @@
1
- require File.dirname(__FILE__) + '/../../../helper'
2
- require 'test/unit'
3
- require 'jinx/helpers/uniquifier'
4
-
5
- class UniquifierTest < Test::Unit::TestCase
6
- def test_uniquify
7
- u1 = Jinx::Uniquifier.instance.uniquify('Groucho')
8
- u2 = Jinx::Uniquifier.instance.uniquify('Groucho')
9
- assert_not_equal(u1, u2, "Consequetive uniquifier calls not unique")
10
- end
11
- end