jinx 2.1.1 → 2.1.2

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.
Files changed (43) hide show
  1. data/History.md +4 -0
  2. data/lib/jinx.rb +0 -1
  3. data/lib/jinx/cli/application.rb +3 -3
  4. data/lib/jinx/cli/command.rb +1 -1
  5. data/lib/jinx/helpers/array.rb +3 -3
  6. data/lib/jinx/helpers/associative.rb +41 -0
  7. data/lib/jinx/helpers/collections.rb +0 -1
  8. data/lib/jinx/helpers/enumerable.rb +5 -2
  9. data/lib/jinx/helpers/hash.rb +1 -1
  10. data/lib/jinx/helpers/hashable.rb +1 -7
  11. data/lib/jinx/helpers/inflector.rb +1 -1
  12. data/lib/jinx/helpers/log.rb +63 -11
  13. data/lib/jinx/helpers/math.rb +20 -11
  14. data/lib/jinx/helpers/options.rb +4 -4
  15. data/lib/jinx/helpers/pretty_print.rb +0 -1
  16. data/lib/jinx/helpers/transitive_closure.rb +1 -1
  17. data/lib/jinx/helpers/uniquifier.rb +50 -17
  18. data/lib/jinx/helpers/visitor.rb +3 -3
  19. data/lib/jinx/import/java.rb +1 -1
  20. data/lib/jinx/importer.rb +3 -2
  21. data/lib/jinx/metadata/attribute_enumerator.rb +1 -1
  22. data/lib/jinx/metadata/dependency.rb +3 -3
  23. data/lib/jinx/metadata/introspector.rb +46 -42
  24. data/lib/jinx/metadata/inverse.rb +17 -1
  25. data/lib/jinx/metadata/java_property.rb +10 -10
  26. data/lib/jinx/metadata/propertied.rb +19 -16
  27. data/lib/jinx/metadata/property.rb +11 -11
  28. data/lib/jinx/resource.rb +86 -14
  29. data/lib/jinx/resource/match_visitor.rb +7 -5
  30. data/lib/jinx/resource/merge_visitor.rb +3 -10
  31. data/lib/jinx/resource/mergeable.rb +16 -16
  32. data/lib/jinx/resource/reference_enumerator.rb +0 -1
  33. data/lib/jinx/resource/reference_path_visitor.rb +1 -1
  34. data/lib/jinx/resource/reference_visitor.rb +5 -6
  35. data/lib/jinx/resource/unique.rb +1 -1
  36. data/lib/jinx/version.rb +1 -1
  37. data/test/lib/jinx/helpers/associative_test.rb +26 -0
  38. data/test/lib/jinx/helpers/collections_test.rb +14 -2
  39. data/test/lib/jinx/helpers/uniquifier_test.rb +11 -0
  40. metadata +9 -8
  41. data/Gemfile.lock +0 -27
  42. data/lib/jinx/helpers/error.rb +0 -15
  43. data/lib/jinx/helpers/key_transformer_hash.rb +0 -43
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.2 / 2012-06-12
5
+ ------------------
6
+ * Fine-tune introspection.
7
+
4
8
  2.1.1 / 2012-04-13
5
9
  ------------------
6
10
  * Initial public release spun off from caruby/core.
@@ -1,3 +1,2 @@
1
1
  require 'jinx/helpers/log'
2
- require 'jinx/helpers/error'
3
2
  require 'jinx/resource'
@@ -19,16 +19,16 @@ module Jinx
19
19
  # * improve the output messages
20
20
  # * print an exception to stderr as well as the log
21
21
  def start(*args, &block)
22
- status = 1
22
+ rc = 1
23
23
  begin
24
- status = run(*args, &block)
24
+ rc = run(*args, &block)
25
25
  rescue
26
26
  log(FATAL, "#{@appname} detected an exception: #{$!}\n#{$@.qp}")
27
27
  msg = "#{@appname} was unsuccessful: #{$!}."
28
28
  msg += "\nSee the log #{Log.instance.file} for more information." if Log.instance.file
29
29
  $stderr.puts msg
30
30
  ensure
31
- log(INFO, "#{@appname} completed with status #{status}.")
31
+ log(INFO, "#{@appname} completed with status #{rc}.")
32
32
  end
33
33
  end
34
34
  end
@@ -105,7 +105,7 @@ module Jinx
105
105
 
106
106
  # @param [{Symbol => Object}] opts the option => value hash
107
107
  def call_executor(opts)
108
- if @executor.nil? then Jinx.fail(CommandError, "Command #{self} does not have an execution block") end
108
+ if @executor.nil? then raise CommandError.new("Command #{self} does not have an execution block") end
109
109
  @executor.call(opts)
110
110
  end
111
111
 
@@ -4,7 +4,7 @@ class Array
4
4
  # The EMPTY_ARRAY constant is an immutable empty array, used primarily as a default argument.
5
5
  class << EMPTY_ARRAY ||= Array.new
6
6
  def <<(value)
7
- Jinx.fail(NotImplementedError, "Modification of the constant empty array is not supported")
7
+ raise NotImplementedError.new("Modification of the constant empty array is not supported")
8
8
  end
9
9
  end
10
10
 
@@ -64,7 +64,7 @@ class Array
64
64
  def to_assoc_hash
65
65
  hash = {}
66
66
  each do |item|
67
- Jinx.fail(ArgumentError, "Array member must be an array: #{item.pp_s(:single_line)}") unless Array === item
67
+ raise ArgumentError.new("Array member must be an array: #{item.pp_s(:single_line)}") unless Array === item
68
68
  key = item.first
69
69
  if item.size < 2 then
70
70
  value = nil
@@ -100,7 +100,7 @@ class Array
100
100
  rescue NoMethodError
101
101
  raise e
102
102
  rescue
103
- Jinx.fail(ArgumentError, "Can't convert #{other.class.name} to array")
103
+ raise ArgumentError.new("Can't convert #{other.class.name} to array")
104
104
  end
105
105
  end
106
106
 
@@ -0,0 +1,41 @@
1
+ require 'jinx/helpers/options'
2
+ module Jinx
3
+ # An Associative object implements a {#[]} method.
4
+ class Associative
5
+ # @yield [key] the associated oject
6
+ # @yieldparam key the key to find
7
+ def initialize(&accessor)
8
+ @accessor = accessor
9
+ end
10
+
11
+ # @param key the key to find
12
+ # @return the associated object
13
+ def [](key)
14
+ @accessor.call(key)
15
+ end
16
+
17
+ # @yield [key] the associated oject
18
+ # @yieldparam key the key to find
19
+ # @return [Associative] a new Associative with a +[]=+ writer method
20
+ def writer(&writer)
21
+ Writable.new(self, &writer)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ class Writable < Associative
28
+ def initialize(base, &writer)
29
+ @base = base
30
+ @writer = writer
31
+ end
32
+
33
+ def [](key)
34
+ @base[key]
35
+ end
36
+
37
+ def []=(key, value)
38
+ @writer.call(key, value)
39
+ end
40
+ end
41
+ end
@@ -1,5 +1,4 @@
1
1
  # This file loads the definitions of useful collection mix-ins and utility classes.
2
-
3
2
  require 'jinx/helpers/collection'
4
3
  require 'jinx/helpers/array'
5
4
  require 'jinx/helpers/hashable'
@@ -31,7 +31,7 @@ module Enumerable
31
31
  # @raise [ArgumentError] if the generator block is not given
32
32
  # @see #hashify
33
33
  def to_compact_hash
34
- Jinx.fail(ArgumentError, "Compact hash builder is missing the value generator block") unless block_given?
34
+ raise ArgumentError.new("Compact hash builder is missing the value generator block") unless block_given?
35
35
  to_compact_hash_with_index { |item, index| yield item }
36
36
  end
37
37
 
@@ -159,6 +159,9 @@ module Enumerable
159
159
  # Returns an Enumerable which iterates over items in this Enumerable and the other Enumerable in sequence.
160
160
  # Unlike the Array plus (+) operator, {#union} reflects changes to the underlying enumerators.
161
161
  #
162
+ # @quirk Cucumber Cucumber defines it's own Enumerable union monkey-patch. Work around this in the short
163
+ # term by trying to call the super first.
164
+ #
162
165
  # @example
163
166
  # a = [1, 2]
164
167
  # b = [4, 5]
@@ -170,7 +173,7 @@ module Enumerable
170
173
  # @param [Enumerable] other the Enumerable to compose with this Enumerable
171
174
  # @return [Enumerable] an enumerator over self followed by other
172
175
  def union(other)
173
- Jinx::MultiEnumerator.new(self, other)
176
+ super rescue Jinx::MultiEnumerator.new(self, other)
174
177
  end
175
178
 
176
179
  alias :+ :union
@@ -6,7 +6,7 @@ class Hash
6
6
  # The EMPTY_HASH constant is an immutable empty hash, used primarily as a default argument.
7
7
  class << EMPTY_HASH ||= Hash.new
8
8
  def []=(key, value)
9
- Jinx.fail(NotImplementedError, "Modification of the constant empty hash is not supported")
9
+ raise NotImplementedError.new("Modification of the constant empty hash is not supported")
10
10
  end
11
11
  end
12
12
  end
@@ -392,7 +392,7 @@ module Jinx
392
392
  attr_reader :components
393
393
 
394
394
  def initialize(*hashes)
395
- if hashes.include?(nil) then Jinx.fail(ArgumentError, "MultiHash is missing a component hash.") end
395
+ if hashes.include?(nil) then raise ArgumentError.new("MultiHash is missing a component hash.") end
396
396
  @components = hashes
397
397
  end
398
398
 
@@ -465,12 +465,6 @@ module Jinx
465
465
  @xfm = transformer
466
466
  end
467
467
 
468
- # @param key the untransformed hash key
469
- # @return the value for the transformed key
470
- def [](key)
471
- @base[@xfm.call(@base[key])]
472
- end
473
-
474
468
  # @yield [key, value] operate on the transformed key and value
475
469
  # @yieldparam key the transformed hash key
476
470
  # @yieldparam value the hash value
@@ -7,7 +7,7 @@ class String
7
7
  # "rose".quantify(3) #=> "roses"
8
8
  # "rose".quantify(1 #=> "rose"
9
9
  def quantify(quantity)
10
- Jinx.fail(ArgumentError, "Missing quantity argument") if quantity.nil?
10
+ raise ArgumentError.new("Missing quantity argument") if quantity.nil?
11
11
  "#{quantity} #{quantity == 1 ? self : pluralize}"
12
12
  end
13
13
 
@@ -3,15 +3,19 @@ require 'singleton'
3
3
  require 'ftools'
4
4
  require 'jinx/helpers/collections'
5
5
  require 'jinx/helpers/options'
6
+ require 'jinx/helpers/inflector'
6
7
 
8
+ # @param [String, IO, nil] dev the optional log file or device
7
9
  # @return [Jinx::MultilineLogger] the global logger
8
- def logger
9
- Jinx.logger
10
+ def logger(dev=nil, opts=nil)
11
+ Jinx.logger(dev, opts)
10
12
  end
11
13
 
12
14
  module Jinx
13
- # @return (see Log#logger)
14
- def self.logger
15
+ # @param [String, IO, nil] dev the optional log file or device
16
+ # @return [Jinx::MultilineLogger] the global logger
17
+ def self.logger(dev=nil, opts=nil)
18
+ Log.instance.open(dev, opts) if dev or opts
15
19
  Log.instance.logger
16
20
  end
17
21
 
@@ -45,17 +49,27 @@ module Jinx
45
49
  class Log
46
50
  include Singleton
47
51
 
48
- # Opens the log.
52
+ # Opens the log. The default log location is determined from the application name.
53
+ # The application name is the value of the +:app+ option, or +Jinx+ by default.
54
+ # For an application +MyApp+, the log location is determined as follows:
55
+ # * +/var/log/my_app.log+ for Linux
56
+ # * +%LOCALAPPDATA%\MyApp\log\MyApp.log+ for Windows
57
+ # * +./log/MyApp.log+ otherwise
58
+ # The default file must be creatable or writable. If the device argument is not
59
+ # provided and there is no suitable default log file, then logging is disabled.
49
60
  #
50
- # @param [String, IO, nil] dev the log file or device (default STDOUT)
61
+ # @param [String, IO, nil] dev the log file or device
51
62
  # @param [Hash, nil] opts the logger options
63
+ # @option opts [String] :app the application name
52
64
  # @option opts [Integer] :shift_age the number of log files retained in the rotation
53
65
  # @option opts [Integer] :shift_size the maximum size of each log file
54
66
  # @option opts [Boolean] :debug whether to include debug messages in the log file
55
67
  # @return [MultilineLogger] the global logger
56
68
  def open(dev=nil, opts=nil)
57
69
  raise RuntimeError.new("Log already open") if open?
58
- if String === dev then File.makedirs(File.dirname(dev)) end
70
+ dev, opts = nil, dev if Hash === dev
71
+ dev ||= default_log_file(Options.get(:app, opts))
72
+ FileUtils.mkdir_p(File.dirname(dev)) if String === dev
59
73
  # default is 4-file rotation @ 16MB each
60
74
  shift_age = Options.get(:shift_age, opts, 4)
61
75
  shift_size = Options.get(:shift_size, opts, 16 * 1048576)
@@ -97,10 +111,48 @@ module Jinx
97
111
  private
98
112
 
99
113
  # Stream-lined log format.
100
- FORMAT = %{%s [%s] %5s %s\n}
114
+ FORMAT = %{%s [%s] %5s %s\n}
115
+
116
+ # The default log file.
117
+ LINUX_LOG_DIR = '/var/log'
101
118
 
102
- def same_file?(f1, f2)
103
- f1 == f2 or (String === f2 and String === f1 and File.expand_path(f1) == File.expand_path(f2))
119
+ # Returns the log file, as described in {#open}.
120
+ #
121
+ # If the standard Linux log location exists, then try that.
122
+ # Otherwise, try the conventional Windows app data location.
123
+ # If all else fails, use the working directory.
124
+ #
125
+ # The file must be creatable or writable.
126
+ #
127
+ # @param [String, nil] app the application name (default +jinx+)
128
+ # @return [String] the file name
129
+ def default_log_file(app=nil)
130
+ app ||= 'Jinx'
131
+ default_linux_log_file(app) || default_windows_log_file(app) || "log/#{app}.log"
132
+ end
133
+
134
+ # @param [String] app the application name
135
+ # @return [String, nil] the default file name
136
+ def default_linux_log_file(app)
137
+ return unless File.exists?(LINUX_LOG_DIR)
138
+ base = app.underscore.gsub(' ', '_')
139
+ file = File.expand_path("#{base}.log", LINUX_LOG_DIR)
140
+ log = file if File.exists?(file) ? File.writable?(file) : File.writable?(LINUX_LOG_DIR)
141
+ log || '/dev/null'
142
+ end
143
+
144
+ # @param [String] app the application name
145
+ # @return [String, nil] the default file name
146
+ def default_windows_log_file(app)
147
+ # the conventional Windows app data location
148
+ app_dir = ENV['LOCALAPPDATA'] || return
149
+ dir = app_dir + "/#{app}/log"
150
+ file = File.expand_path("#{app}.log", dir)
151
+ if File.exists?(file) ? File.writable?(file) : (File.directory?(dir) ? File.writable?(dir) : File.writable?(app_dir)) then
152
+ file
153
+ else
154
+ 'NUL'
155
+ end
104
156
  end
105
157
  end
106
- end
158
+ end
@@ -1,12 +1,21 @@
1
- # Extends the Numeric class with max and min methods.
2
- class Numeric
3
- # Returns the minimum of this Numeric and the other Numerics.
4
- def min(*others)
5
- others.inject(self) { |min, other| other < min ? other : min }
1
+ module Jinx
2
+ module Math
3
+ # @param [<Numeric>] the numbers to compare
4
+ # @return [Numeric] the smallest number
5
+ def self.min(*args)
6
+ args.inject { |m, n| m < n ? m : n }
7
+ end
8
+
9
+ # @param [<Numeric>] the numbers to compare
10
+ # @return [Numeric] the largest number
11
+ def self.max(*args)
12
+ args.inject { |m, n| m < n ? n : m }
13
+ end
14
+
15
+ # @param value the value to check
16
+ # @return [Boolean] whether the value is a Ruby or Java number
17
+ def self.numeric?(value)
18
+ Numeric === value or Java::JavaLang::Number === value
19
+ end
6
20
  end
7
-
8
- # Returns the minimum of this Numeric and the other Numerics.
9
- def max(*others)
10
- others.inject(self) { |max, other| other > max ? other : max }
11
- end
12
- end
21
+ end
@@ -17,7 +17,7 @@ class Options
17
17
  # If default is nil and a block is given to this method, then the default is determined
18
18
  # by calling the block with no arguments. The block can also be used to raise a missing
19
19
  # option exception, e.g.:
20
- # Options.get(:userid, options) { Jinx.fail(RuntimeError, "Missing required option: userid") }
20
+ # Options.get(:userid, options) { raise RuntimeError.new("Missing required option: userid") }
21
21
  #
22
22
  # @example
23
23
  # Options.get(:create, {:create => true}) #=> true
@@ -41,7 +41,7 @@ class Options
41
41
  when Symbol then
42
42
  option == options or default(default, &block)
43
43
  else
44
- Jinx.fail(ArgumentError, "Options argument type is not supported; expected Hash or Symbol, found: #{options.class}")
44
+ raise ArgumentError.new("Options argument type is not supported; expected Hash or Symbol, found: #{options.class}")
45
45
  end
46
46
  end
47
47
 
@@ -64,7 +64,7 @@ class Options
64
64
  case opt
65
65
  when Symbol then hash[opt] = true
66
66
  when Hash then hash.merge!(opt)
67
- else Jinx.fail(ArgumentError, "Expected a symbol or hash option, found #{opt.qp}")
67
+ else raise ArgumentError.new("Expected a symbol or hash option, found #{opt.qp}")
68
68
  end
69
69
  end
70
70
  hash
@@ -74,7 +74,7 @@ class Options
74
74
  # @raise [ValidationError] if the given options are not in the given allowable choices
75
75
  def self.validate(options, choices)
76
76
  to_hash(options).each_key do |opt|
77
- Jinx.fail(ValidationError, "Option is not supported: #{opt}") unless choices.include?(opt)
77
+ raise ValidationError.new("Option is not supported: #{opt}") unless choices.include?(opt)
78
78
  end
79
79
  end
80
80
 
@@ -4,7 +4,6 @@ require 'pp'
4
4
  require 'stringio'
5
5
  require 'jinx/helpers/options'
6
6
  require 'jinx/helpers/collections'
7
-
8
7
  require 'jinx/helpers/inflector'
9
8
 
10
9
  class PrettyPrint
@@ -29,7 +29,7 @@ class Object
29
29
  # a.transitive_closure { |node| node.children }.to_a.join(", ") #=> a, b, c, d
30
30
  # a.transitive_closure(:children).to_a.join(", ") #=> a, b, c, d
31
31
  def transitive_closure(method=nil)
32
- Jinx.fail(ArgumentError, "Missing both a method argument and a block") if method.nil? and not block_given?
32
+ raise ArgumentError.new("Missing both a method argument and a block") if method.nil? and not block_given?
33
33
  # If there is a method argument, then the transitive closure is based on that method.
34
34
  # Otherwise, visit the closure in reverse depth-first order.
35
35
  if method then
@@ -24,27 +24,60 @@ module Jinx
24
24
  def initialize
25
25
  @cache = Jinx::LazyHash.new { Hash.new }
26
26
  end
27
-
28
- # @param obj the object containing the value
29
- # @param value the value to make unique
30
- # @return the new unique value, or nil if the given value is nil
31
- def uniquify(obj, value)
32
- @cache[obj.class][value] ||= value.uniquify if value
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
33
58
  end
34
59
 
35
60
  def clear
36
61
  @cache.clear
37
62
  end
38
- end
39
- end
40
-
41
- class String
42
- # Returns a relatively unique value obtained from the specified base value.
43
- # The suffix is generated by {Jinx::Uniquifier.qualifier}. Spaces are removed.
44
- #
45
- # @example
46
- # 'Test Name'.uniquify #=> Test_Name_330938800614
47
- def uniquify
48
- gsub(' ', '_') + "_#{Jinx::Uniquifier.qualifier}"
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
49
82
  end
50
83
  end