jinx 2.1.1

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 (149) hide show
  1. data/.gitignore +14 -0
  2. data/.rspec +3 -0
  3. data/.yardopts +1 -0
  4. data/Gemfile +6 -0
  5. data/Gemfile.lock +27 -0
  6. data/History.md +6 -0
  7. data/LEGAL +5 -0
  8. data/LICENSE +22 -0
  9. data/README.md +44 -0
  10. data/Rakefile +41 -0
  11. data/examples/family/README.md +10 -0
  12. data/examples/family/ext/build.xml +35 -0
  13. data/examples/family/ext/src/family/Address.java +68 -0
  14. data/examples/family/ext/src/family/Child.java +24 -0
  15. data/examples/family/ext/src/family/DomainObject.java +26 -0
  16. data/examples/family/ext/src/family/Household.java +36 -0
  17. data/examples/family/ext/src/family/Parent.java +48 -0
  18. data/examples/family/ext/src/family/Person.java +42 -0
  19. data/examples/family/lib/family.rb +15 -0
  20. data/examples/family/lib/family/address.rb +6 -0
  21. data/examples/family/lib/family/domain_object.rb +6 -0
  22. data/examples/family/lib/family/household.rb +6 -0
  23. data/examples/family/lib/family/parent.rb +16 -0
  24. data/examples/family/lib/family/person.rb +6 -0
  25. data/examples/model/README.md +25 -0
  26. data/examples/model/ext/build.xml +35 -0
  27. data/examples/model/ext/src/domain/Child.java +192 -0
  28. data/examples/model/ext/src/domain/Dependent.java +29 -0
  29. data/examples/model/ext/src/domain/DomainObject.java +26 -0
  30. data/examples/model/ext/src/domain/Independent.java +83 -0
  31. data/examples/model/ext/src/domain/Parent.java +129 -0
  32. data/examples/model/ext/src/domain/Person.java +14 -0
  33. data/examples/model/lib/model.rb +13 -0
  34. data/examples/model/lib/model/child.rb +13 -0
  35. data/examples/model/lib/model/domain_object.rb +6 -0
  36. data/examples/model/lib/model/independent.rb +11 -0
  37. data/examples/model/lib/model/parent.rb +17 -0
  38. data/jinx.gemspec +22 -0
  39. data/lib/jinx.rb +3 -0
  40. data/lib/jinx/active_support/README.txt +2 -0
  41. data/lib/jinx/active_support/core_ext/string.rb +7 -0
  42. data/lib/jinx/active_support/core_ext/string/inflections.rb +167 -0
  43. data/lib/jinx/active_support/inflections.rb +55 -0
  44. data/lib/jinx/active_support/inflector.rb +398 -0
  45. data/lib/jinx/cli/application.rb +36 -0
  46. data/lib/jinx/cli/command.rb +214 -0
  47. data/lib/jinx/helpers/array.rb +108 -0
  48. data/lib/jinx/helpers/boolean.rb +42 -0
  49. data/lib/jinx/helpers/case_insensitive_hash.rb +39 -0
  50. data/lib/jinx/helpers/class.rb +149 -0
  51. data/lib/jinx/helpers/collection.rb +33 -0
  52. data/lib/jinx/helpers/collections.rb +11 -0
  53. data/lib/jinx/helpers/collector.rb +20 -0
  54. data/lib/jinx/helpers/conditional_enumerator.rb +21 -0
  55. data/lib/jinx/helpers/enumerable.rb +242 -0
  56. data/lib/jinx/helpers/enumerate.rb +35 -0
  57. data/lib/jinx/helpers/error.rb +15 -0
  58. data/lib/jinx/helpers/file_separator.rb +65 -0
  59. data/lib/jinx/helpers/filter.rb +52 -0
  60. data/lib/jinx/helpers/flattener.rb +38 -0
  61. data/lib/jinx/helpers/hash.rb +12 -0
  62. data/lib/jinx/helpers/hashable.rb +502 -0
  63. data/lib/jinx/helpers/inflector.rb +36 -0
  64. data/lib/jinx/helpers/key_transformer_hash.rb +43 -0
  65. data/lib/jinx/helpers/lazy_hash.rb +44 -0
  66. data/lib/jinx/helpers/log.rb +106 -0
  67. data/lib/jinx/helpers/math.rb +12 -0
  68. data/lib/jinx/helpers/merge.rb +60 -0
  69. data/lib/jinx/helpers/module.rb +18 -0
  70. data/lib/jinx/helpers/multi_enumerator.rb +31 -0
  71. data/lib/jinx/helpers/options.rb +92 -0
  72. data/lib/jinx/helpers/os.rb +19 -0
  73. data/lib/jinx/helpers/partial_order.rb +37 -0
  74. data/lib/jinx/helpers/pretty_print.rb +207 -0
  75. data/lib/jinx/helpers/set.rb +8 -0
  76. data/lib/jinx/helpers/stopwatch.rb +76 -0
  77. data/lib/jinx/helpers/transformer.rb +24 -0
  78. data/lib/jinx/helpers/transitive_closure.rb +55 -0
  79. data/lib/jinx/helpers/uniquifier.rb +50 -0
  80. data/lib/jinx/helpers/validation.rb +33 -0
  81. data/lib/jinx/helpers/visitor.rb +370 -0
  82. data/lib/jinx/import/class_path_modifier.rb +77 -0
  83. data/lib/jinx/import/java.rb +337 -0
  84. data/lib/jinx/importer.rb +240 -0
  85. data/lib/jinx/metadata.rb +155 -0
  86. data/lib/jinx/metadata/attribute_enumerator.rb +73 -0
  87. data/lib/jinx/metadata/dependency.rb +244 -0
  88. data/lib/jinx/metadata/id_alias.rb +23 -0
  89. data/lib/jinx/metadata/introspector.rb +179 -0
  90. data/lib/jinx/metadata/inverse.rb +170 -0
  91. data/lib/jinx/metadata/java_property.rb +169 -0
  92. data/lib/jinx/metadata/propertied.rb +500 -0
  93. data/lib/jinx/metadata/property.rb +401 -0
  94. data/lib/jinx/metadata/property_characteristics.rb +114 -0
  95. data/lib/jinx/resource.rb +862 -0
  96. data/lib/jinx/resource/copy_visitor.rb +36 -0
  97. data/lib/jinx/resource/inversible.rb +90 -0
  98. data/lib/jinx/resource/match_visitor.rb +180 -0
  99. data/lib/jinx/resource/matcher.rb +20 -0
  100. data/lib/jinx/resource/merge_visitor.rb +73 -0
  101. data/lib/jinx/resource/mergeable.rb +185 -0
  102. data/lib/jinx/resource/reference_enumerator.rb +49 -0
  103. data/lib/jinx/resource/reference_path_visitor.rb +38 -0
  104. data/lib/jinx/resource/reference_visitor.rb +55 -0
  105. data/lib/jinx/resource/unique.rb +35 -0
  106. data/lib/jinx/version.rb +3 -0
  107. data/spec/defaults_spec.rb +30 -0
  108. data/spec/definitions/model/alias/child.rb +5 -0
  109. data/spec/definitions/model/base/child.rb +5 -0
  110. data/spec/definitions/model/base/domain_object.rb +5 -0
  111. data/spec/definitions/model/base/independent.rb +5 -0
  112. data/spec/definitions/model/defaults/child.rb +5 -0
  113. data/spec/definitions/model/dependency/child.rb +5 -0
  114. data/spec/definitions/model/dependency/parent.rb +6 -0
  115. data/spec/definitions/model/inverse/child.rb +5 -0
  116. data/spec/definitions/model/inverse/independent.rb +5 -0
  117. data/spec/definitions/model/inverse/parent.rb +5 -0
  118. data/spec/definitions/model/mandatory/child.rb +6 -0
  119. data/spec/dependency_spec.rb +47 -0
  120. data/spec/family_spec.rb +64 -0
  121. data/spec/inverse_spec.rb +53 -0
  122. data/spec/mandatory_spec.rb +43 -0
  123. data/spec/metadata_spec.rb +68 -0
  124. data/spec/resource_spec.rb +30 -0
  125. data/spec/spec_helper.rb +3 -0
  126. data/spec/support/model.rb +19 -0
  127. data/test/fixtures/line_separator/cr_line_sep.txt +1 -0
  128. data/test/fixtures/line_separator/crlf_line_sep.txt +3 -0
  129. data/test/fixtures/line_separator/lf_line_sep.txt +3 -0
  130. data/test/fixtures/mixed/ext/build.xml +35 -0
  131. data/test/fixtures/mixed/ext/src/mixed/Case/Example.java +5 -0
  132. data/test/helper.rb +7 -0
  133. data/test/lib/jinx/command_test.rb +41 -0
  134. data/test/lib/jinx/helpers/boolean_test.rb +27 -0
  135. data/test/lib/jinx/helpers/class_test.rb +60 -0
  136. data/test/lib/jinx/helpers/collections_test.rb +402 -0
  137. data/test/lib/jinx/helpers/file_separator_test.rb +29 -0
  138. data/test/lib/jinx/helpers/inflector_test.rb +11 -0
  139. data/test/lib/jinx/helpers/lazy_hash_test.rb +32 -0
  140. data/test/lib/jinx/helpers/module_test.rb +24 -0
  141. data/test/lib/jinx/helpers/options_test.rb +66 -0
  142. data/test/lib/jinx/helpers/partial_order_test.rb +41 -0
  143. data/test/lib/jinx/helpers/pretty_print_test.rb +83 -0
  144. data/test/lib/jinx/helpers/stopwatch_test.rb +16 -0
  145. data/test/lib/jinx/helpers/transitive_closure_test.rb +80 -0
  146. data/test/lib/jinx/helpers/visitor_test.rb +288 -0
  147. data/test/lib/jinx/import/java_test.rb +78 -0
  148. data/test/lib/jinx/import/mixed_case_test.rb +16 -0
  149. metadata +272 -0
@@ -0,0 +1,398 @@
1
+ # encoding: utf-8
2
+ require 'singleton'
3
+ require 'iconv'
4
+
5
+ module ActiveSupport
6
+ # The Inflector transforms words from singular to plural, class names to table names, modularized class names to ones without,
7
+ # and class names to foreign keys. The default inflections for pluralization, singularization, and uncountable words are kept
8
+ # in inflections.rb.
9
+ #
10
+ # The Rails core team has stated patches for the inflections library will not be accepted
11
+ # in order to avoid breaking legacy applications which may be relying on errant inflections.
12
+ # If you discover an incorrect inflection and require it for your application, you'll need
13
+ # to correct it yourself (explained below).
14
+ module Inflector
15
+ extend self
16
+
17
+ # A singleton instance of this class is yielded by {Inflector#inflections}, which can then be used to specify additional
18
+ # inflection rules. Examples:
19
+ #
20
+ # ActiveSupport::Inflector.inflections do |inflect|
21
+ # inflect.plural /^(ox)$/i, '\12en'
22
+ # inflect.singular /^(ox)en/i, '1'
23
+ #
24
+ # inflect.irregular 'octopus', 'octopi'
25
+ #
26
+ # inflect.uncountable "equipment"
27
+ # end
28
+ #
29
+ # New rules are added at the top. So in the example above, the irregular rule for octopus will now be the first of the
30
+ # pluralization and singularization rules that is runs. This guarantees that your rules run before any of the rules that may
31
+ # already have been loaded.
32
+ class Inflections
33
+ include Singleton
34
+
35
+ attr_reader :plurals, :singulars, :uncountables, :humans
36
+
37
+ def initialize
38
+ @plurals, @singulars, @uncountables, @humans = [], [], [], []
39
+ end
40
+
41
+ # Specifies a new pluralization rule and its replacement. The rule can either be a string or a regular expression.
42
+ # The replacement should always be a string that may include references to the matched data from the rule.
43
+ def plural(rule, replacement)
44
+ @uncountables.delete(rule) if String === rule
45
+ @uncountables.delete(replacement)
46
+ @plurals.insert(0, [rule, replacement])
47
+ end
48
+
49
+ # Specifies a new singularization rule and its replacement. The rule can either be a string or a regular expression.
50
+ # The replacement should always be a string that may include references to the matched data from the rule.
51
+ def singular(rule, replacement)
52
+ @uncountables.delete(rule) if String === rule
53
+ @uncountables.delete(replacement)
54
+ @singulars.insert(0, [rule, replacement])
55
+ end
56
+
57
+ # Specifies a new irregular that applies to both pluralization and singularization at the same time. This can only be used
58
+ # for strings, not regular expressions. You simply pass the irregular in singular and plural form.
59
+ #
60
+ # @example
61
+ # irregular 'octopus', 'octopi'
62
+ # irregular 'person', 'people'
63
+ def irregular(singular, plural)
64
+ @uncountables.delete(singular)
65
+ @uncountables.delete(plural)
66
+ if singular[0,1].upcase == plural[0,1].upcase
67
+ plural(Regexp.new("(#{singular[0,1]})#{singular[1..-1]}$", "i"), '\1' + plural[1..-1])
68
+ singular(Regexp.new("(#{plural[0,1]})#{plural[1..-1]}$", "i"), '\1' + singular[1..-1])
69
+ else
70
+ plural(Regexp.new("#{singular[0,1].upcase}(?i)#{singular[1..-1]}$"), plural[0,1].upcase + plural[1..-1])
71
+ plural(Regexp.new("#{singular[0,1].downcase}(?i)#{singular[1..-1]}$"), plural[0,1].downcase + plural[1..-1])
72
+ singular(Regexp.new("#{plural[0,1].upcase}(?i)#{plural[1..-1]}$"), singular[0,1].upcase + singular[1..-1])
73
+ singular(Regexp.new("#{plural[0,1].downcase}(?i)#{plural[1..-1]}$"), singular[0,1].downcase + singular[1..-1])
74
+ end
75
+ end
76
+
77
+ # Add uncountable words that shouldn't be attempted inflected.
78
+ #
79
+ # @example
80
+ # uncountable "money"
81
+ # uncountable "money", "information"
82
+ # uncountable %w( money information rice )
83
+ def uncountable(*words)
84
+ (@uncountables << words).flatten!
85
+ end
86
+
87
+ # Specifies a humanized form of a string by a regular expression rule or by a string mapping.
88
+ # When using a regular expression based replacement, the normal humanize formatting is called after the replacement.
89
+ # When a string is used, the human form should be specified as desired (example: 'The name', not 'the_name')
90
+ #
91
+ # @example
92
+ # human /_cnt$/i, '1_count'
93
+ # human "legacy_col_person_name", "Name"
94
+ def human(rule, replacement)
95
+ @humans.insert(0, [rule, replacement])
96
+ end
97
+
98
+ # Clears the loaded inflections within a given scope (default is <tt>:all</tt>).
99
+ # Give the scope as a symbol of the inflection type, the options are: <tt>:plurals</tt>,
100
+ # <tt>:singulars</tt>, <tt>:uncountables</tt>, <tt>:humans</tt>.
101
+ #
102
+ # @example
103
+ # clear :all
104
+ # clear :plurals
105
+ def clear(scope = :all)
106
+ case scope
107
+ when :all
108
+ @plurals, @singulars, @uncountables = [], [], []
109
+ else
110
+ instance_variable_set "@#{scope}", []
111
+ end
112
+ end
113
+ end
114
+
115
+ # Yields a singleton instance of Inflector::Inflections so you can specify additional
116
+ # inflector rules.
117
+ #
118
+ # Example:
119
+ # ActiveSupport::Inflector.inflections do |inflect|
120
+ # inflect.uncountable "rails"
121
+ # end
122
+ def inflections
123
+ if block_given?
124
+ yield Inflections.instance
125
+ else
126
+ Inflections.instance
127
+ end
128
+ end
129
+
130
+ # Returns the plural form of the word in the string.
131
+ #
132
+ # @example
133
+ # "post".pluralize #=>"posts"
134
+ # "octopus".pluralize #=>"octopi"
135
+ # "sheep".pluralize #=>"sheep"
136
+ # "words".pluralize #=>"words"
137
+ # "CamelOctopus".pluralize #=>"CamelOctopi"
138
+ def pluralize(word)
139
+ result = word.to_s.dup
140
+
141
+ if word.empty? || inflections.uncountables.include?(result.downcase)
142
+ result
143
+ else
144
+ inflections.plurals.each { |(rule, replacement)| break if result.gsub!(rule, replacement) }
145
+ result
146
+ end
147
+ end
148
+
149
+ # The reverse of +pluralize+, returns the singular form of a word in a string.
150
+ #
151
+ # @example
152
+ # "posts".singularize #=>"post"
153
+ # "octopi".singularize #=>"octopus"
154
+ # "sheep".singluarize #=>"sheep"
155
+ # "word".singularize #=>"word"
156
+ # "CamelOctopi".singularize #=>"CamelOctopus"
157
+ def singularize(word)
158
+ result = word.to_s.dup
159
+
160
+ if inflections.uncountables.include?(result.downcase)
161
+ result
162
+ else
163
+ inflections.singulars.each { |(rule, replacement)| break if result.gsub!(rule, replacement) }
164
+ result
165
+ end
166
+ end
167
+
168
+ # By default, +camelize+ converts strings to UpperCamelCase. If the argument to +camelize+
169
+ # is set to <tt>:lower</tt> then +camelize+ produces lowerCamelCase.
170
+ #
171
+ # +camelize+ will also convert '/' to '::' which is useful for converting paths to namespaces.
172
+ #
173
+ # @example
174
+ # "active_record".camelize #=>"ActiveRecord"
175
+ # "active_record".camelize(:lower) #=>"activeRecord"
176
+ # "active_record/errors".camelize #=>"ActiveRecord::Errors"
177
+ # "active_record/errors".camelize(:lower) #=>"activeRecord::Errors"
178
+ def camelize(lower_case_and_underscored_word, first_letter_in_uppercase = true)
179
+ if first_letter_in_uppercase
180
+ lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::#{$1.upcase }" }.gsub(/(?:^|_)(.)/) { $1.upcase }
181
+ else
182
+ # FL - fixed bug: String doesn't have a first method
183
+ #lower_case_and_underscored_word.first.downcase + camelize(lower_case_and_underscored_word)[1..-1]
184
+ lower_case_and_underscored_word[0, 1].downcase + camelize(lower_case_and_underscored_word)[1..-1]
185
+ end
186
+ end
187
+
188
+ # Capitalizes all the words and replaces some characters in the string to create
189
+ # a nicer looking title. +titleize+ is meant for creating pretty output. It is not
190
+ # used in the Rails internals.
191
+ #
192
+ # +titleize+ is also aliased as as +titlecase+.
193
+ #
194
+ # Examples:
195
+ # "man from the boondocks".titleize #=>"Man From The Boondocks"
196
+ # "x-men: the last stand".titleize #=>"X Men: The Last Stand"
197
+ def titleize(word)
198
+ humanize(underscore(word)).gsub(/\b('?[a-z])/) { $1.capitalize }
199
+ end
200
+
201
+ # The reverse of +camelize+. Makes an underscored, lowercase form from the expression in the string.
202
+ #
203
+ # Changes '::' to '/' to convert namespaces to paths.
204
+ #
205
+ # Examples:
206
+ # "ActiveRecord".underscore #=>"active_record"
207
+ # "ActiveRecord::Errors".underscore #=>active_record/errors
208
+ def underscore(camel_cased_word)
209
+ camel_cased_word.to_s.gsub(/::/, '/').
210
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
211
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
212
+ tr("-", "_").
213
+ downcase
214
+ end
215
+
216
+ # Replaces underscores with dashes in the string.
217
+ #
218
+ # Example:
219
+ # "puni_puni" #=>"puni-puni"
220
+ def dasherize(underscored_word)
221
+ underscored_word.gsub(/_/, '-')
222
+ end
223
+
224
+ # Capitalizes the first word and turns underscores into spaces and strips a
225
+ # trailing "_id", if any. Like +titleize+, this is meant for creating pretty output.
226
+ #
227
+ # Examples:
228
+ # "employee_salary" #=>"Employee salary"
229
+ # "author_id" #=>"Author"
230
+ def humanize(lower_case_and_underscored_word)
231
+ result = lower_case_and_underscored_word.to_s.dup
232
+
233
+ inflections.humans.each { |(rule, replacement)| break if result.gsub!(rule, replacement) }
234
+ result.gsub(/_id$/, "").gsub(/_/, " ").capitalize
235
+ end
236
+
237
+ # Removes the module part from the expression in the string.
238
+ #
239
+ # Examples:
240
+ # "ActiveRecord::CoreExtensions::String::Inflections".demodulize #=>"Inflections"
241
+ # "Inflections".demodulize #=>"Inflections"
242
+ def demodulize(class_name_in_module)
243
+ class_name_in_module.to_s.gsub(/^.*::/, '')
244
+ end
245
+
246
+ # Replaces special characters in a string so that it may be used as part of a 'pretty' URL.
247
+ #
248
+ # ==== Examples
249
+ #
250
+ # class Person
251
+ # def to_param
252
+ # "#{id}-#{name.parameterize}"
253
+ # end
254
+ # end
255
+ #
256
+ # @person = Person.find(1)
257
+ # #=>#<Person id: 1, name: "Donald E. Knuth">
258
+ #
259
+ # <%= link_to(@person.name, person_path %>
260
+ # #=><a href="/person/1-donald-e-knuth">Donald E. Knuth</a>
261
+ def parameterize(string, sep = '-')
262
+ re_sep = Regexp.escape(sep)
263
+ # replace accented chars with ther ascii equivalents
264
+ parameterized_string = transliterate(string)
265
+ # Turn unwanted chars into the seperator
266
+ parameterized_string.gsub!(/[^a-z0-9\-_\+]+/i, sep)
267
+ # No more than one of the separator in a row.
268
+ parameterized_string.squeeze!(sep)
269
+ # Remove leading/trailing separator.
270
+ parameterized_string.gsub!(/^#{re_sep}|#{re_sep}$/i, '')
271
+ parameterized_string.downcase
272
+ end
273
+
274
+ # Replaces accented characters with their ascii equivalents.
275
+ def transliterate(string)
276
+ Iconv.iconv('ascii//ignore//translit', 'utf-8', string).to_s
277
+ end
278
+
279
+ # The iconv transliteration code doesn't function correctly
280
+ # on some platforms, but it's very fast where it does function.
281
+ if "foo" != Inflector.transliterate("föö")
282
+ undef_method :transliterate
283
+ def transliterate(string)
284
+ string.mb_chars.normalize(:kd). # Decompose accented characters
285
+ gsub(/[^\x00-\x7F]+/, '') # Remove anything non-ASCII entirely (e.g. diacritics).
286
+ end
287
+ end
288
+
289
+ # Create the name of a table like Rails does for models to table names. This method
290
+ # uses the +pluralize+ method on the last word in the string.
291
+ #
292
+ # Examples
293
+ # "RawScaledScorer".tableize #=>"raw_scaled_scorers"
294
+ # "egg_and_ham".tableize #=>"egg_and_hams"
295
+ # "fancyCategory".tableize #=>"fancy_categories"
296
+ def tableize(class_name)
297
+ pluralize(underscore(class_name))
298
+ end
299
+
300
+ # Create a class name from a plural table name like Rails does for table names to models.
301
+ # Note that this returns a string and not a Class. (To convert to an actual class
302
+ # follow +classify+ with +constantize+.)
303
+ #
304
+ # Examples:
305
+ # "egg_and_hams".classify #=>"EggAndHam"
306
+ # "posts".classify #=>"Post"
307
+ #
308
+ # Singular names are not handled correctly:
309
+ # "business".classify #=>"Busines"
310
+ def classify(table_name)
311
+ # strip out any leading schema name
312
+ camelize(singularize(table_name.to_s.sub(/.*\./, '')))
313
+ end
314
+
315
+ # Creates a foreign key name from a class name.
316
+ # +separate_class_name_and_id_with_underscore+ sets whether
317
+ # the method should put '_' between the name and 'id'.
318
+ #
319
+ # Examples:
320
+ # "Message".foreign_key #=>"message_id"
321
+ # "Message".foreign_key(false) #=>"messageid"
322
+ # "Admin::Post".foreign_key #=>"post_id"
323
+ def foreign_key(class_name, separate_class_name_and_id_with_underscore = true)
324
+ underscore(demodulize(class_name)) + (separate_class_name_and_id_with_underscore ? "_id" : "id")
325
+ end
326
+
327
+ # Ruby 1.9 introduces an inherit argument for {Module#const_get} and
328
+ # #const_defined? and changes their default behavior.
329
+ if Module.method(:const_get).arity == 1
330
+ # Tries to find a constant with the name specified in the argument string:
331
+ #
332
+ # "Module".constantize #=>Module
333
+ # "Test::Unit".constantize #=>Test::Unit
334
+ #
335
+ # The name is assumed to be the one of a top-level constant, no matter whether
336
+ # it starts with "::" or not. No lexical context is taken into account:
337
+ #
338
+ # C = 'outside'
339
+ # module M
340
+ # C = 'inside'
341
+ # C #=>'inside'
342
+ # "C".constantize #=>'outside', same as ::C
343
+ # end
344
+ #
345
+ # NameError is raised when the name is not in CamelCase or the constant is
346
+ # unknown.
347
+ def constantize(camel_cased_word)
348
+ names = camel_cased_word.split('::')
349
+ names.shift if names.empty? || names.first.empty?
350
+
351
+ constant = Object
352
+ names.each do |name|
353
+ constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
354
+ end
355
+ constant
356
+ end
357
+ else
358
+ def constantize(camel_cased_word) #:nodoc:
359
+ names = camel_cased_word.split('::')
360
+ names.shift if names.empty? || names.first.empty?
361
+
362
+ constant = Object
363
+ names.each do |name|
364
+ constant = constant.const_get(name, false) || constant.const_missing(name)
365
+ end
366
+ constant
367
+ end
368
+ end
369
+
370
+ # Turns a number into an ordinal string used to denote the position in an
371
+ # ordered sequence such as 1st, 2nd, 3rd, 4th.
372
+ #
373
+ # Examples:
374
+ # ordinalize(1) #=>"1st"
375
+ # ordinalize(2) #=>"2nd"
376
+ # ordinalize(1002) #=>"1002nd"
377
+ # ordinalize(1003) #=>"1003rd"
378
+ def ordinalize(number)
379
+ if (11..13).include?(number.to_i % 100)
380
+ "#{number}th"
381
+ else
382
+ case number.to_i % 10
383
+ when 1; "#{number}st"
384
+ when 2; "#{number}nd"
385
+ when 3; "#{number}rd"
386
+ else "#{number}th"
387
+ end
388
+ end
389
+ end
390
+ end
391
+ end
392
+
393
+ # in case jinx/active_support/inflector is required without the rest of active_support
394
+ require 'jinx/active_support/inflections'
395
+ require 'jinx/active_support/core_ext/string/inflections'
396
+ unless String.included_modules.include?(ActiveSupport::CoreExtensions::String::Inflections)
397
+ String.send :include, ActiveSupport::CoreExtensions::String::Inflections
398
+ end
@@ -0,0 +1,36 @@
1
+ require 'logger'
2
+
3
+ module Jinx
4
+ module CLI
5
+ # Extends the standard Logger::Application to use the {Log} and add start
6
+ # functionality.
7
+ class Application < Logger::Application
8
+ # @param [String] appname the application name
9
+ def initialize(appname=nil)
10
+ super(appname)
11
+ # set the application logger
12
+ @log = logger
13
+ @log.progname = @appname
14
+ @level = @log.level
15
+ end
16
+
17
+ # Overrides Logger::Application start with the following enhancements:
18
+ # * pass arguments and a block to the application run method
19
+ # * improve the output messages
20
+ # * print an exception to stderr as well as the log
21
+ def start(*args, &block)
22
+ status = 1
23
+ begin
24
+ status = run(*args, &block)
25
+ rescue
26
+ log(FATAL, "#{@appname} detected an exception: #{$!}\n#{$@.qp}")
27
+ msg = "#{@appname} was unsuccessful: #{$!}."
28
+ msg += "\nSee the log #{Log.instance.file} for more information." if Log.instance.file
29
+ $stderr.puts msg
30
+ ensure
31
+ log(INFO, "#{@appname} completed with status #{status}.")
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,214 @@
1
+ require 'optparse'
2
+ require 'jinx/helpers/log'
3
+ require 'jinx/cli/application'
4
+
5
+ module Jinx
6
+ module CLI
7
+ # Open the log file before start-up.
8
+ log_ndx = ARGV.index("--log") || ARGV.index("-l")
9
+ log_file = log_ndx ? ARGV[log_ndx + 1] : ENV['LOG']
10
+ debug = ARGV.include?('--debug') || ENV['DEBUG'] =~ /true/i
11
+ Log.instance.open(log_file, :debug => debug) if log_file or debug
12
+
13
+ # Command-line parsing errors.
14
+ class CommandError < StandardError; end
15
+
16
+ # Command-line parser and executor.
17
+ class Command < Application
18
+ # Command line application wrapper.
19
+ # The specs parameter is an array of command line option and argument
20
+ # specifications as follows:
21
+ #
22
+ # The option specification has format:
23
+ #
24
+ # [_option_, _short_, _long_, _class_, _description_]
25
+ #
26
+ # where:
27
+ # * _option_ is the option symbol, e.g. +:output+
28
+ # * _short_ is the short option form, e.g. "-o"
29
+ # * _long_ is the long option form, e.g. "--output FILE"
30
+ # * _class_ is the option value class, e.g. Integer
31
+ # * _description_ is the option usage, e.g. "Output file"
32
+ # The _option_, _long_ and _description_ items are required; the _short_
33
+ # and _class_ items can be omitted.
34
+ #
35
+ # The argument specification is an array in the form:
36
+ #
37
+ # [_arg_, _text_]
38
+ #
39
+ # where:
40
+ # * _arg_ is the argument symbol, e.g. +:input+
41
+ # * _text_ is the usage message text, e.g. 'input', '[input]' or 'input ...'
42
+ # Both _arg_ and _text_ are required.
43
+ #
44
+ # Built-in options include the following:
45
+ # * +--help+ : print the help message and exit
46
+ # * +--verbose+ : print additional information to the console
47
+ # * +--log FILE+ : log file
48
+ # * +--debug+ : print debug messages to the log
49
+ # * +--file FILE+: file containing other options
50
+ # * +--quiet+: suppress printing messages to stdout
51
+ #
52
+ # This class processes these built-in options. Subclasses are responsible for
53
+ # processing any remaining options.
54
+ #
55
+ # @param [<(Symbol, String, String, Class, String), (Symbol, String)>, nil] specs
56
+ # the command line argument specifications
57
+ # @yield (see #run)
58
+ # @yieldparam (see #run)
59
+ def initialize(specs=Array::EMPTY_ARRAY, &executor)
60
+ @executor = executor
61
+ # Validate the specifications.
62
+ unless Array === specs then
63
+ raise ArgumentError.new("Command-line specification is not an array: #{specs.qp}")
64
+ end
65
+ invalid = specs.detect { |spec| spec.size < 2 }
66
+ if invalid then
67
+ raise ArgumentError.new("Command-line argument specification is missing text: #{invalid.qp}")
68
+ end
69
+ # Options start with a dash, arguments are whatever is left.
70
+ @opt_specs, @arg_specs = specs.partition { |spec| spec[1][0, 1] == '-' }
71
+ # Add the default option specifications.
72
+ @opt_specs.concat(DEF_OPTS)
73
+ # The application name is the command.
74
+ super(File.basename($0, ".bat"))
75
+ end
76
+
77
+ # Runs this command by calling the block given to this method, if provided,
78
+ # otherwise the block given to {#initialize}
79
+ # option or argument symbol => value hash.
80
+ # @yield [hash] the command execution block
81
+ # @yieldparam [{Symbol => Object}] hash the argument and option symbol => value hash
82
+ def run
83
+ # the option => value hash
84
+ opts = get_opts
85
+ # this base class's options
86
+ handle_options(opts)
87
+ # add the argument => value hash
88
+ opts.merge!(get_args)
89
+ # call the block
90
+ log(INFO, "Starting #{@appname}...")
91
+ block_given? ? yield(opts) : call_executor(opts)
92
+ end
93
+
94
+ private
95
+
96
+ # The default options that apply to all commands.
97
+ DEF_OPTS = [
98
+ [:help, "-h", "--help", "Display this help message"],
99
+ [:file, "--file FILE", "Configuration file containing other options"],
100
+ [:log, "--log FILE", "Log file"],
101
+ [:debug, "--debug", "Display debug log messages"],
102
+ [:quiet, "-q", "--quiet", "Suppress printing messages to stdout"],
103
+ [:verbose, "-v", "--verbose", "Print additional messages to stdout"]
104
+ ]
105
+
106
+ # @param [{Symbol => Object}] opts the option => value hash
107
+ def call_executor(opts)
108
+ if @executor.nil? then Jinx.fail(CommandError, "Command #{self} does not have an execution block") end
109
+ @executor.call(opts)
110
+ end
111
+
112
+ # Collects the command line options.
113
+ #
114
+ # @return [{Symbol => Object}] the option => value hash
115
+ def get_opts
116
+ # the options hash
117
+ opts = {}
118
+ # the option parser
119
+ OptionParser.new do |parser|
120
+ # The help argument string is comprised of the argument specification labels.
121
+ arg_s = @arg_specs.map { |spec| spec[1] }.join(' ')
122
+ # Build the usage message.
123
+ parser.banner = "Usage: #{parser.program_name} [options] #{arg_s}"
124
+ parser.separator ""
125
+ parser.separator "Options:"
126
+ # parse the options
127
+ opts = parse(parser)
128
+ # grab the usage message
129
+ @usage = parser.help
130
+ end
131
+ opts
132
+ end
133
+
134
+ # Collects the non-option command line arguments.
135
+ #
136
+ # @return [{Symbol => Object}] the argument => value hash
137
+ def get_args
138
+ return Hash::EMPTY_HASH if ARGV.empty?
139
+ if @arg_specs.empty? then too_many_arguments end
140
+ # Collect the arguments from the command line.
141
+ args = {}
142
+ # The number of command line arguments or all but the last argument specifications,
143
+ # whichever is less. The last argument can have more than one value, indicated by
144
+ # the argument specification form '...', so it is processed separately below.
145
+ n = [ARGV.size, @arg_specs.size - 1].min
146
+ # the single-valued arguments
147
+ n.times { |i| args[@arg_specs[i].first] = ARGV[i] }
148
+ # Process the last argument.
149
+ if n < ARGV.size then
150
+ spec = @arg_specs.last
151
+ arg, form = spec[0], spec[1]
152
+ # A multi-valued last argument is the residual command argument array.
153
+ # A single-valued last argument is the last value, if there is exactly one.
154
+ # Otherwise, there are too many arguments.
155
+ if form.index('...') then
156
+ args[arg] = ARGV[n..-1]
157
+ elsif @arg_specs.size == ARGV.size then
158
+ args[arg] = ARGV[n]
159
+ else
160
+ too_many_arguments
161
+ end
162
+ end
163
+ args
164
+ end
165
+
166
+ def too_many_arguments
167
+ halt("Too many arguments - expected #{@arg_specs.size}, found: #{ARGV.join(' ')}.", 1)
168
+ end
169
+
170
+ # @param [OptionParser] parser the option parser
171
+ # @return [{Symbol => Object}] the option => value hash
172
+ def parse(parser)
173
+ opts = {}
174
+ @opt_specs.each do |opt, *spec|
175
+ parser.on_tail(*spec) { |v| opts[opt] = v }
176
+ end
177
+ # build the option => value hash
178
+ parser.parse!
179
+ opts
180
+ end
181
+
182
+ # Processes the built-in options as follows:
183
+ # * +:help+ - print the usage message and exit
184
+ # * +:file+ FILE - load the options specified in the given file
185
+ #
186
+ # @param [{Symbol => Object}] the option => value hash
187
+ def handle_options(opts)
188
+ # if help, then print usage and exit
189
+ if opts[:help] then halt end
190
+ # If there is a file option, then load additional options from the file.
191
+ file = opts.delete(:file)
192
+ if file then
193
+ fopts = File.open(file).map { |line| line.chomp }.split(' ').flatten
194
+ ARGV.concat(fopts)
195
+ OptionParser.new do |p|
196
+ opts.merge!(parse(p)) { |ov, nv| ov ? ov : nv }
197
+ end
198
+ end
199
+ end
200
+
201
+ # Prints the given error message and the program usage, then exits with status 1.
202
+ def fail(message=nil)
203
+ halt(message, 1)
204
+ end
205
+
206
+ # Prints the given message and program usage, then exits with the given status.
207
+ def halt(message=nil, status=0)
208
+ puts(message) if message
209
+ puts(@usage)
210
+ exit(status)
211
+ end
212
+ end
213
+ end
214
+ end