jinx 2.1.1 → 2.1.2

Sign up to get free protection for your applications and to get access to all the features.
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