moab-versioning 2.1.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 314bede5bec369d49b3173b4cb6ca49b67dad86c
4
- data.tar.gz: 2efefdc7fa3a28b69e45778fd1a373be555d4a4b
3
+ metadata.gz: 8a0b643706a4149747c76abbbbdb8458a73c62ed
4
+ data.tar.gz: 485960168209b4768a76deb35fbd56360a8b32e3
5
5
  SHA512:
6
- metadata.gz: 26daf2d5fa9f5a859676313a180ba5c73acb2e441b73cdfb6cdafc847e043521764e469512f364dead19fb0ea5d3d095f5e666d97f3362112ca08cb8b5d93685
7
- data.tar.gz: 9638f3dcec4715d837abde7bc3c7f28713553692482b1ea3b93c3d654000bd6fb0cc6100af01266bc0aec94863c3f006ae9177117b4d3667af38accc67dd4925
6
+ metadata.gz: 26f4fee9182c007e5c68fdef45a53fbb75330dfcdc9ed39e02e719a095a1a4d684adbb2ab7ff049fa15631cf1e72711f78b3b6ee2633a99f31c264f2b77ddb05
7
+ data.tar.gz: 31c2bc1b8120546686d6952b06ab2b20b046d4f010a829bb546bc3c4669832ac15ef627b388dd4414e6c59e2628b1737f7a1c2dc1d07c37c3d26db9f365babea
@@ -90,9 +90,11 @@ module Moab
90
90
  end
91
91
 
92
92
  # @api external
93
- # @param package_mode [Symbol] The operational mode controlling what gets bagged and the full path of source files (Bagger#fill_payload)
93
+ # @param package_mode [Symbol] The operational mode controlling what gets bagged and the full path of
94
+ # source files (Bagger#fill_payload)
94
95
  # @param source_base_pathname [Pathname] The home location of the source files
95
- # @return [Bagger] Perform all the operations required to fill the bag payload, write the manifests and tagfiles, and checksum the tagfiles
96
+ # @return [Bagger] Perform all the operations required to fill the bag payload, write the manifests and
97
+ # tagfiles, and checksum the tagfiles
96
98
  # @example {include:file:spec/features/storage/deposit_spec.rb}
97
99
  def fill_bag(package_mode, source_base_pathname)
98
100
  create_bag_inventory(package_mode)
@@ -102,7 +104,8 @@ module Moab
102
104
  end
103
105
 
104
106
  # @api external
105
- # @param package_mode [Symbol] The operational mode controlling what gets bagged and the full path of source files (Bagger#fill_payload)
107
+ # @param package_mode [Symbol] The operational mode controlling what gets bagged and the full path of
108
+ # source files (Bagger#fill_payload)
106
109
  # @return [FileInventory] Create, write, and return the inventory of the files that will become the payload
107
110
  def create_bag_inventory(package_mode)
108
111
  @package_mode = package_mode
@@ -155,11 +155,10 @@ module Moab
155
155
 
156
156
  # @return [Pathname] The full path used as the basis of the relative paths reported
157
157
  # in {FileInstance} objects that are children of the {FileManifestation} objects contained in this file group
158
- attr_accessor :base_directory
159
-
160
158
  def base_directory=(basepath)
161
159
  @base_directory = Pathname.new(basepath).expand_path
162
160
  end
161
+ attr_reader :base_directory
163
162
 
164
163
  # @api internal
165
164
  # @param pathname [Pathname] The file path to be tested
@@ -241,4 +240,3 @@ module Moab
241
240
  end
242
241
 
243
242
  end
244
-
@@ -63,7 +63,8 @@ module Moab
63
63
  attribute :group_id, String, :tag => 'groupId', :key => true
64
64
 
65
65
  # @attribute
66
- # @return [Integer] the total number of differences found between the two inventories that were compared (dynamically calculated)
66
+ # @return [Integer] the total number of differences found between the two inventories that were
67
+ # compared (dynamically calculated)
67
68
  attribute :difference_count, Integer, :tag => 'differenceCount', :on_save => Proc.new { |i| i.to_s }
68
69
 
69
70
  def difference_count
@@ -263,14 +264,13 @@ module Moab
263
264
  fid.basis_path = basis_only_paths[n]
264
265
  fid.other_path = other_only_paths[n]
265
266
  fid.signatures << signature
266
- case true
267
- when fid.basis_path.nil?
268
- fid.change = 'copyadded'
269
- fid.basis_path = basis_paths[0]
270
- when fid.other_path.nil?
271
- fid.change = 'copydeleted'
272
- else
273
- fid.change = 'renamed'
267
+ if fid.basis_path.nil?
268
+ fid.change = 'copyadded'
269
+ fid.basis_path = basis_paths[0]
270
+ elsif fid.other_path.nil?
271
+ fid.change = 'copydeleted'
272
+ else
273
+ fid.change = 'renamed'
274
274
  end
275
275
  @subset_hash[fid.change.to_sym].files << fid
276
276
  end
@@ -5,7 +5,8 @@ module Moab
5
5
  # A container for recording difference information at the file level
6
6
  # * If there was no change, the change type is set to <i>identical</i>
7
7
  # * If the signature is unchanged, but the path has moved, the change type is set to <i>renamed</i>
8
- # * If path is unchanged, but the signature has changed, the change type is set to <i>modified</i> and both signatures are reported
8
+ # * If path is unchanged, but the signature has changed, the change type is set to <i>modified</i> and
9
+ # both signatures are reported
9
10
  # * If the signature and path are only in the basis inventory, the change type is set to <i>deleted</i>
10
11
  # * If the signature and path are only in the other inventory, the change type is set to <i>added</i>
11
12
  # This is a child element of {FileGroupDifferenceSubset}, which is in turn a descendent of {FileInventoryDifference},
@@ -51,4 +52,4 @@ module Moab
51
52
 
52
53
  end
53
54
 
54
- end
55
+ end
@@ -134,9 +134,11 @@ module Moab
134
134
  # @return [FileSignature] The signature of the specified file
135
135
  def file_signature(group_id, file_id)
136
136
  file_group = group(group_id)
137
- raise FileNotFoundException, "group #{group_id} not found for #{@digital_object_id} - #{@version_id}" if file_group.nil?
137
+ errmsg = "group #{group_id} not found for #{@digital_object_id} - #{@version_id}"
138
+ raise FileNotFoundException, errmsg if file_group.nil?
138
139
  file_signature = file_group.path_hash[file_id]
139
- raise FileNotFoundException, "#{group_id} file #{file_id} not found for #{@digital_object_id} - #{@version_id}" if file_signature.nil?
140
+ errmsg = "#{group_id} file #{file_id} not found for #{@digital_object_id} - #{@version_id}"
141
+ raise FileNotFoundException, errmsg if file_signature.nil?
140
142
  file_signature
141
143
  end
142
144
 
@@ -182,19 +184,20 @@ module Moab
182
184
  # if nil, then the directory is assumed to contain both content and metadata subdirectories
183
185
  # @return [FileInventory] Traverse a directory and return an inventory of the files it contains
184
186
  # @example {include:file:spec/features/inventory/harvest_inventory_spec.rb}
185
- def inventory_from_directory(data_dir,group_id=nil)
187
+ def inventory_from_directory(data_dir, group_id=nil)
186
188
  if group_id
187
- @groups << FileGroup.new(:group_id=>group_id).group_from_directory(data_dir)
189
+ @groups << FileGroup.new(group_id: group_id).group_from_directory(data_dir)
188
190
  else
189
- ['content','metadata'].each do |group_id|
190
- @groups << FileGroup.new(:group_id=>group_id).group_from_directory(Pathname(data_dir).join(group_id))
191
+ ['content', 'metadata'].each do |gid|
192
+ @groups << FileGroup.new(group_id: gid).group_from_directory(Pathname(data_dir).join(gid))
191
193
  end
192
194
  end
193
195
  self
194
196
  end
195
197
 
196
198
  # @param bag_dir [Pathname,String] The location of the BagIt bag to be inventoried
197
- # @return [FileInventory] Traverse a BagIt bag's payload and return an inventory of the files it contains (using fixity from bag manifest files)
199
+ # @return [FileInventory] Traverse a BagIt bag's payload and return an inventory of the files it contains
200
+ # (using fixity from bag manifest files)
198
201
  def inventory_from_bagit_bag(bag_dir)
199
202
  bag_pathname = Pathname(bag_dir)
200
203
  signatures_from_bag = signatures_from_bagit_manifests(bag_pathname)
@@ -261,7 +264,7 @@ module Moab
261
264
  when "directory"
262
265
  'directoryInventory.xml'
263
266
  else
264
- raise "unknown inventory type: #{type.to_s}"
267
+ raise ArgumentError, "unknown inventory type: #{type.to_s}"
265
268
  end
266
269
  end
267
270
 
@@ -79,7 +79,7 @@ module Moab
79
79
  when :sha256
80
80
  @sha256 = value
81
81
  else
82
- raise "Unknown checksum type '#{type.to_s}'"
82
+ raise ArgumentError, "Unknown checksum type '#{type.to_s}'"
83
83
  end
84
84
  end
85
85
 
@@ -115,7 +115,8 @@ module Moab
115
115
  # @return [Pathname] The absolute storage path of the file, including the object's home directory
116
116
  def storage_filepath(catalog_filepath)
117
117
  storage_filepath = @object_pathname.join(catalog_filepath)
118
- raise FileNotFoundException, "#{catalog_filepath} missing from storage location #{storage_filepath}" unless storage_filepath.exist?
118
+ errmsg = "#{catalog_filepath} missing from storage location #{storage_filepath}"
119
+ raise FileNotFoundException, errmsg unless storage_filepath.exist?
119
120
  storage_filepath
120
121
  end
121
122
 
@@ -132,7 +133,7 @@ module Moab
132
133
  return list unless @object_pathname.exist?
133
134
  @object_pathname.children.each do |dirname|
134
135
  vnum = dirname.basename.to_s
135
- if vnum.match /^v(\d+)$/
136
+ if vnum =~ /^v(\d+)$/
136
137
  list << vnum[1..-1].to_i
137
138
  end
138
139
  end
@@ -153,11 +154,7 @@ module Moab
153
154
  # @api external
154
155
  # @return [Integer] The identifier of the latest version of this object, or 0 if no versions exist
155
156
  def current_version_id
156
- return @current_version_id unless @current_version_id.nil?
157
- #@current_version_id = self.version_id_list.last || 0
158
- list = self.version_id_list
159
- version_id = list.empty? ? 0 : list.last
160
- @current_version_id = version_id
157
+ @current_version_id ||= self.version_id_list.last || 0
161
158
  end
162
159
 
163
160
  # @return [StorageObjectVersion] The most recent version in the storage object
@@ -218,7 +215,7 @@ module Moab
218
215
  # @return [Boolean] Restore all recovered versions to online storage and verify results
219
216
  def restore_object(recovery_path)
220
217
  timestamp = Time.now
221
- recovery_object = StorageObject.new(@digital_object_id, recovery_path, mkpath=false)
218
+ recovery_object = StorageObject.new(@digital_object_id, recovery_path, false)
222
219
  recovery_object.versions.each do |recovery_version|
223
220
  version_id = recovery_version.version_id
224
221
  storage_version = self.storage_object_version(version_id)
@@ -230,6 +227,14 @@ module Moab
230
227
  self
231
228
  end
232
229
 
230
+ # @return [Integer] the size occupied on disk by the storage object, in bytes. this is the entire moab (all versions).
231
+ def size
232
+ size = 0
233
+ Find.find(object_pathname) do |path|
234
+ size += FileTest.size(path) unless FileTest.directory?(path)
235
+ end
236
+ size
237
+ end
233
238
  end
234
239
 
235
240
  end
@@ -204,8 +204,9 @@ module Moab
204
204
  :type=>'manifests',
205
205
  :digital_object_id=>@storage_object.digital_object_id,
206
206
  :version_id=>@version_id)
207
- manifest_inventory.groups << FileGroup.new(:group_id=>'manifests').group_from_directory(@version_pathname.join('manifests'), recursive=false)
208
- manifest_inventory.write_xml_file(@version_pathname.join('manifests'))
207
+ pathname = @version_pathname.join('manifests')
208
+ manifest_inventory.groups << FileGroup.new(:group_id=>'manifests').group_from_directory(pathname, false)
209
+ manifest_inventory.write_xml_file(pathname)
209
210
  end
210
211
 
211
212
  # @return [VerificationResult] return result of testing correctness of version manifests
@@ -330,7 +331,5 @@ module Moab
330
331
  @version_pathname.rename(demote_pathame)
331
332
  end
332
333
  end
333
-
334
334
  end
335
-
336
335
  end
@@ -15,8 +15,9 @@ module Moab
15
15
  # All rights reserved. See {file:LICENSE.rdoc} for details.
16
16
  class StorageRepository
17
17
 
18
- #Note that Moab::Config is not initialized from environment config file until after this object is initialized by StorageServices
19
- # (see sequence of requires in spec_helper.rb and in applications that use)
18
+ # Note that Moab::Config is not initialized from environment config file until after
19
+ # this object is initialized by StorageServices
20
+ # (see sequence of requires in spec_helper.rb and in applications that use)
20
21
 
21
22
  # @return [Array<Pathname>] The list of filesystem root paths in which objects are stored
22
23
  def storage_roots
@@ -45,7 +46,7 @@ module Moab
45
46
  # split a object ID into 2-character segments, followed by a copy of the object ID
46
47
  # for a more sophisticated pairtree implementation see https://github.com/microservices/pairtree
47
48
  path_segments = object_id.scan(/..?/) << object_id
48
- path_segments.join(File::SEPARATOR).gsub(/:/,'_')
49
+ path_segments.join(File::SEPARATOR).tr(':', '_')
49
50
  end
50
51
 
51
52
  # @return [String] The trunk segment of the object deposit path
@@ -109,11 +110,7 @@ module Moab
109
110
  storage_pathname = find_storage_object(object_id, include_deposit).object_pathname
110
111
  size = 0
111
112
  Find.find(storage_pathname) do |path|
112
- if FileTest.directory?(path)
113
- Find.prune if File.basename(path)[0] == '.'
114
- else
115
- size += FileTest.size(path)
116
- end
113
+ size += FileTest.size(path) unless FileTest.directory?(path)
117
114
  end
118
115
  size
119
116
  end
@@ -20,7 +20,8 @@ module Stanford
20
20
  # @return [FileInventory] The versionInventory equivalent of the contentMetadata
21
21
  # if the supplied content_metadata is blank or empty, then a skeletal FileInventory will be returned
22
22
  def inventory_from_cm(content_metadata, object_id, subset, version_id=nil)
23
- # The contentMetadata datastream is not required for ingest, since some object types, such as collection or APO do not require one.
23
+ # The contentMetadata datastream is not required for ingest, since some object types, such as collection
24
+ # or APO do not require one.
24
25
  # Many of these objects have contentMetadata with no child elements, such as this:
25
26
  # <contentMetadata objectId="bd608mj3166" type="file"/>
26
27
  # but there are also objects that have no datasteam of this name at all
@@ -33,7 +33,7 @@ module Stanford
33
33
  # @return [FileInventory] Inventory of the files under the specified directory
34
34
  def inventory_from_directory(directory, version_id=nil)
35
35
  version_id ||= @version_id
36
- version_inventory = Moab::FileInventory.new(:type=>'version',:digital_object_id=>@digital_object_id, :version_id=>version_id)
36
+ version_inventory = Moab::FileInventory.new(type: 'version', digital_object_id: @digital_object_id, version_id: version_id)
37
37
  content_metadata = IO.read(File.join(directory,'contentMetadata.xml'))
38
38
  content_group = Stanford::ContentInventory.new.group_from_cm(content_metadata, 'preserve' )
39
39
  version_inventory.groups << content_group
@@ -41,9 +41,6 @@ module Stanford
41
41
  version_inventory.groups << metadata_group
42
42
  version_inventory
43
43
  end
44
-
45
-
46
-
47
44
  end
48
45
 
49
- end
46
+ end
@@ -1,4 +1,5 @@
1
1
  require 'moab/stanford'
2
+ require 'druid-tools'
2
3
 
3
4
  module Stanford
4
5
 
@@ -15,10 +16,10 @@ module Stanford
15
16
  # @return [String] The branch segment of the object storage path
16
17
  def storage_branch(object_id)
17
18
  case Moab::Config.path_method.to_s
18
- when 'druid_tree'
19
- druid_tree(object_id)
20
- when 'druid'
21
- object_id.split(/:/)[-1]
19
+ when 'druid_tree'
20
+ druid_tree(object_id)
21
+ when 'druid'
22
+ object_id.split(/:/)[-1]
22
23
  end
23
24
  end
24
25
 
@@ -31,22 +32,15 @@ module Stanford
31
32
  # @param object_id [String] The identifier of the digital object whose path is requested
32
33
  # @return [String] the druid tree directory path based on the given object identifier.
33
34
  def druid_tree(object_id)
35
+ # Note: this seems an odd place to do druid validation, but leaving it here for now
34
36
  syntax_msg = "Identifier has invalid suri syntax: #{object_id}"
35
- raise syntax_msg + "nil or empty" if object_id.to_s.empty?
37
+ raise syntax_msg + " nil or empty" if object_id.to_s.empty?
36
38
  identifier = object_id.split(':')[-1]
37
- raise syntax_msg if identifier.to_s.empty?
38
- # The object identifier must be in the SURI format, otherwise an exception is raised:
39
- # e.g. druid:aannnaannnn or aannnaannnn
40
- # where 'a' is an alphabetic character
41
- # where 'n' is a numeric character
42
- if identifier =~ /^([a-z]{2})(\d{3})([a-z]{2})(\d{4})$/
43
- return File.join( $1, $2, $3, $4, identifier)
44
- else
45
- raise syntax_msg
46
- end
39
+ raise syntax_msg if identifier.to_s.empty?
40
+ raise syntax_msg unless DruidTools::Druid.valid?(identifier, true)
41
+ DruidTools::Druid.new(identifier, true).tree.join(File::SEPARATOR)
47
42
  end
48
43
 
49
-
50
44
  end
51
45
 
52
46
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: moab-versioning
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Darren Weber
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2017-09-07 00:00:00.000000000 Z
14
+ date: 2017-10-20 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: confstruct
@@ -83,6 +83,20 @@ dependencies:
83
83
  - - ">="
84
84
  - !ruby/object:Gem::Version
85
85
  version: '0'
86
+ - !ruby/object:Gem::Dependency
87
+ name: druid-tools
88
+ requirement: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: 1.0.0
93
+ type: :runtime
94
+ prerelease: false
95
+ version_requirements: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: 1.0.0
86
100
  - !ruby/object:Gem::Dependency
87
101
  name: awesome_print
88
102
  requirement: !ruby/object:Gem::Requirement
@@ -201,28 +215,28 @@ dependencies:
201
215
  requirements:
202
216
  - - "~>"
203
217
  - !ruby/object:Gem::Version
204
- version: 0.49.1
218
+ version: 0.50.0
205
219
  type: :development
206
220
  prerelease: false
207
221
  version_requirements: !ruby/object:Gem::Requirement
208
222
  requirements:
209
223
  - - "~>"
210
224
  - !ruby/object:Gem::Version
211
- version: 0.49.1
225
+ version: 0.50.0
212
226
  - !ruby/object:Gem::Dependency
213
227
  name: rubocop-rspec
214
228
  requirement: !ruby/object:Gem::Requirement
215
229
  requirements:
216
230
  - - "~>"
217
231
  - !ruby/object:Gem::Version
218
- version: 1.16.0
232
+ version: 1.18.0
219
233
  type: :development
220
234
  prerelease: false
221
235
  version_requirements: !ruby/object:Gem::Requirement
222
236
  requirements:
223
237
  - - "~>"
224
238
  - !ruby/object:Gem::Version
225
- version: 1.16.0
239
+ version: 1.18.0
226
240
  description: Contains classes to process digital object version content and metadata
227
241
  email:
228
242
  - darren.weber@stanford.edu
@@ -264,9 +278,6 @@ files:
264
278
  - lib/stanford/storage_repository.rb
265
279
  - lib/stanford/storage_services.rb
266
280
  - lib/tasks/yard.rake
267
- - lib/tools/api_doc_generator.rb
268
- - lib/tools/spec_generator.rb
269
- - lib/tools/spec_generator_old.rb
270
281
  homepage: https://github.com/sul-dlss/moab-versioning
271
282
  licenses:
272
283
  - Apache-2.0
@@ -287,7 +298,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
287
298
  version: 1.3.6
288
299
  requirements: []
289
300
  rubyforge_project:
290
- rubygems_version: 2.5.1
301
+ rubygems_version: 2.6.11
291
302
  signing_key:
292
303
  specification_version: 4
293
304
  summary: Ruby implementation of digital object versioning toolkit used by the SULAIR
@@ -1,395 +0,0 @@
1
- #$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', '..', 'lib'))
2
- require 'rubygems'
3
- require 'pathname'
4
- require 'yard'
5
- include YARD
6
- require 'moab/stanford'
7
- include Serializer
8
- include Moab
9
- include Stanford
10
-
11
- class String
12
- def snake_case
13
- self.gsub(/::/, '/').
14
- gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').
15
- gsub(/([a-z\d])([A-Z])/, '\1_\2').
16
- tr("-", "_").
17
- downcase
18
- end
19
- end
20
-
21
- class Symbol
22
- def snake_case
23
- self.to_s.snake_case
24
- end
25
- end
26
-
27
- class ApiClass
28
- attr_accessor :class_name
29
- attr_accessor :yard_class
30
- attr_accessor :mhash
31
- attr_accessor :ruby_class
32
- attr_accessor :xml_tag
33
- attr_accessor :description
34
- attr_accessor :model
35
-
36
- def self.class_hash=(class_hash)
37
- @@class_hash = class_hash
38
- end
39
-
40
- def self.rootpath=(rootpath)
41
- @@rootpath = rootpath
42
- end
43
-
44
- def initialize(class_name)
45
- @class_name = class_name
46
- @yard_class = @@class_hash[class_name]
47
- if @yard_class.nil?
48
- raise "class_hash[#{class_name}] is Nil"
49
- end
50
- @mhash = categorize_members(@yard_class)
51
- if @yard_class.path.include?('::')
52
- ruby_module = Object::const_get(yard_class.path.split(/::/)[0])
53
- @ruby_class = ruby_module.const_get(yard_class.name)
54
- else
55
- @ruby_class = Object::const_get(yard_class.path)
56
- end
57
-
58
- if @ruby_class.respond_to?(:tag_name)
59
- @xml_tag = "<#{ruby_class.tag_name}>"
60
- else
61
- @xml_tag = '-'
62
- end
63
- docstring_all = confluence_translate(yard_class.docstring.all.split(/@/)[0])
64
- docstring_parts = docstring_all.split(/h4. Data Model\n/)
65
- @description = docstring_parts[0]
66
- @model = docstring_parts.size > 1 ? docstring_parts[1] : nil
67
- end
68
-
69
- def categorize_members(yard_class)
70
- mhash = {
71
- :class_attributes => Hash.new,
72
- :instance_attributes => Hash.new,
73
- :class_methods => Array.new,
74
- :instance_methods => Array.new
75
- }
76
- yard_class.children.each do |member|
77
- attr_symbol = member.name.to_s.gsub(/=$/, '').to_sym
78
- if member.name == :initialize
79
- mhash[:constructor] = member
80
- elsif yard_class.class_attributes[attr_symbol]
81
- mhash[:class_attributes][attr_symbol] = yard_class.class_attributes[attr_symbol]
82
- elsif yard_class.instance_attributes[attr_symbol]
83
- mhash[:instance_attributes][attr_symbol] = yard_class.instance_attributes[attr_symbol]
84
- elsif member.scope == :class
85
- mhash[:class_methods] << member
86
- elsif member.scope == :instance
87
- mhash[:instance_methods] << member
88
- end
89
- end
90
- mhash
91
- end
92
-
93
- def title
94
- title = "\n{anchor:#{yard_class.name}}\n"
95
- title << "h3. Class #{yard_class.path}"
96
- title
97
- end
98
-
99
- def description
100
- description = "\nh4. Description\n\n"
101
- description << @description
102
- description
103
- end
104
-
105
- def model
106
- model = "\nh4. Data Model\n\n"
107
- model << @model
108
- model
109
- end
110
-
111
- def xml_example
112
- xml_example = String.new
113
- if yard_class.docstring.has_tag?('example')
114
- xml_example << "\nh4. XML Example\n"
115
- xml_example << "{code:lang=xml}\n"
116
- filename = yard_class.docstring.tag('example').name.split(/[:)}]/)[2]
117
- example = IO.read(File.join(@@rootpath, filename))
118
- xml_example << example
119
- xml_example << "{code}\n"
120
- end
121
- xml_example
122
- end
123
-
124
- def instance_attributes_table(mode=:all)
125
- table = String.new
126
- table << "\\\\\n||XML Element||Ruby Class||Inherits From||\n"
127
- table << "|#{xml_tag}|[##{class_name}]|#{yard_class.superclass}|\n"
128
- table << "\\\\\n||XML Child Node||Ruby Attribute||Data Type||Description||\n"
129
- xml_names = xml_name_hash
130
- @mhash[:instance_attributes].values.each do |attribute|
131
- read = attribute[:read]
132
- return_tag = read.docstring.tag(:return)
133
- ruby_name = read.name.to_s
134
- xml_name = xml_names[ruby_name] ? xml_names[ruby_name] : "-"
135
- data_type = return_tag.types[0]
136
- description = confluence_translate(return_tag.text.gsub(/\n/, ' '))
137
- table_row = "|#{xml_name}|#{ruby_name}|#{data_type}|#{description.gsub(/\|/, '\\|')}|\n"
138
- case mode
139
- when :xml
140
- if xml_name != '-'
141
- table << table_row
142
- end
143
- when :ruby
144
- if xml_name == '-'
145
- table << table_row
146
- end
147
- else
148
- table << table_row
149
- end
150
- end
151
- table
152
- end
153
-
154
- def xml_name_hash
155
- xml_name_hash = Hash.new
156
- if @ruby_class.respond_to?(:attributes)
157
- @ruby_class.attributes.each do |attribute|
158
- xml_name_hash[attribute.name.to_s] = "@#{attribute.tag}"
159
- end
160
- end
161
- if @ruby_class.respond_to?(:elements)
162
- @ruby_class.elements.each do |element|
163
- if element.options.size == 0 or element.options[:single]
164
- xml_name_hash[element.name.to_s] = "<#{element.tag}>"
165
- else
166
- xml_name_hash[element.name.to_s] = "<#{element.tag}> \\[1..\\*]"
167
- end
168
- end
169
- end
170
- xml_name_hash
171
- end
172
-
173
- def methods_documentation
174
-
175
- methods_documentation = String.new
176
-
177
- #methods_documentation ""
178
- #methods_documentation "h4. Constructor"
179
- #method = mhash[:constructor]
180
- #methods_documentation_method(method, yard_class) if method
181
-
182
- methods =mhash[:class_methods]
183
- if methods.size > 0
184
- methods_documentation << "\nh4. Class Methods\n"
185
- methods.each do |method|
186
- methods_documentation << method_documentation(method)
187
- end
188
- end
189
-
190
- methods =mhash[:instance_methods]
191
- if methods.size > 0
192
- public_methods = Array.new
193
- methods.each do |method|
194
- if method.docstring.has_tag?(:api) && method.docstring.tag(:api).text == 'external'
195
- public_methods << method
196
- end
197
- end
198
- if public_methods.size > 0
199
- methods_documentation << "\nh4. Instance Methods\n"
200
- public_methods.each do |method|
201
- methods_documentation << method_documentation(method)
202
- end
203
- end
204
- end
205
- methods_documentation
206
- end
207
-
208
- def method_documentation(method)
209
-
210
- method_documentation = String.new
211
- if method.nil?
212
- raise "method is nil"
213
- end
214
- method_documentation << "\nh5. #{method.path}\n"
215
-
216
- method_documentation << "||Method||Return Type||Description||\n"
217
- if method.name == :initialize
218
- return_type = @yard_class.name
219
- description = 'constructor'
220
- else
221
- return_tag = method.docstring.tag(:return)
222
- if return_tag.nil?
223
- raise "#{method.name} return tag is nil"
224
- end
225
- return_type = return_tag.types[0]
226
- description = confluence_translate(return_tag.text.gsub(/\n/, ' '))
227
- end
228
- method_documentation << "|#{method.name}|#{return_type}|#{example.description}|\n"
229
-
230
- if method.respond_to?(:docstring)
231
- params = method.docstring.tags(:param)
232
- if params && params.size > 0
233
- method_documentation << "\n||Parameter||Data Type||Description||\n"
234
- params.each do |p|
235
- description = confluence_translate(p.text.gsub(/\n/, ' '))
236
- method_documentation << "|#{p.name}|#{p.types.join(', ')}|#{example.description}|\n"
237
- end
238
- end
239
- end
240
-
241
- method_documentation << "{code:lang=none|title=Ruby Source Code}\n"
242
- method_documentation << method.source
243
- method_documentation << "{code}\n"
244
-
245
- if method.docstring.has_tag?('example')
246
- method_documentation << "\n{code:lang=none|title=Usage Example}\n"
247
- filename = method.docstring.tag('example').name.split(/[:)}]/)[2]
248
- example = IO.read(File.join(@@rootpath, filename))
249
- method_documentation << example
250
- method_documentation << "{code}\n"
251
- end
252
- method_documentation
253
- end
254
-
255
- def confluence_translate(input)
256
- map = Hash.new
257
- map[/\|/] = "\\|"
258
- map[/====/] = "h4. "
259
- map[/\*/] = "\\*"
260
- map[/\n\s{6}\\\*\s/] = "\n****\s"
261
- map[/\n\s{4}\\\*\s/] = "\n***\s"
262
- map[/\n\s{2}\\\*\s/] = "\n**\s"
263
- map[/\n\\\*\s/] = "\n*\s"
264
- map[/<[\/]*b>/] = "*"
265
- map[/<[\/]*i>/] = "_"
266
- map[/\[/] = "\\["
267
- map[/\{#/] = "[#"
268
- map[/\{http/] = "[http"
269
- map[/\{/] = "[#"
270
- map[/\}/] = "]"
271
- output = input
272
- map.each do |regex, replacement|
273
- output.gsub!(regex, replacement)
274
- end
275
- output
276
- end
277
-
278
- end
279
-
280
-
281
- class ApiDoc < ApiClass
282
- attr_accessor :ios
283
- attr_accessor :classes
284
-
285
- def process_doc(ios)
286
- @ios = ios
287
- parse_model
288
-
289
- output component
290
- output model
291
- output description
292
- output xml_example
293
- output xml_nodes
294
- classes_detail
295
- end
296
-
297
- def component
298
- component = "h2. Component: #{yard_class.name}\n"
299
- component
300
- end
301
-
302
- def xml_nodes
303
- nodes = "\nh4. XML Nodes\n"
304
- @classes.each do |cls|
305
- nodes << cls.instance_attributes_table(mode=:xml)
306
- end
307
- nodes
308
- end
309
-
310
- def classes_detail
311
- @classes.each do |cls|
312
- output cls.title
313
- output cls.description if cls != self
314
- output cls.methods_documentation
315
- end
316
- end
317
-
318
- def parse_model
319
- @classes = Array.new
320
- @classes << self
321
- if @model
322
- model.lines.each do |line|
323
- matches = line.scan(/\[#(.*)?\]/)
324
- if matches.size > 0
325
- match = matches[0][0].split(/\]/)[0]
326
- if match != class_name
327
- @classes << ApiClass.new(match)
328
- end
329
- end
330
- end
331
- end
332
-
333
- end
334
-
335
- def output(string)
336
- @ios.puts string
337
- end
338
-
339
-
340
- end
341
-
342
-
343
- class DocGenerator
344
-
345
- def initialize(rootpath)
346
- @rootpath = rootpath
347
- end
348
-
349
- def generate_docs(apis)
350
- temp_pathname = Pathname(@rootpath).join('api', 'temp')
351
- ApiClass.class_hash = get_class_hash
352
- ApiClass.rootpath = @rootpath
353
- apis.each do |api_name|
354
- doc_pathname = temp_pathname.join(api_name + "_confluence.txt")
355
- doc_pathname.parent.mkpath
356
- #puts doc_pathname.to_s
357
- #unless doc_pathname.exist?
358
- begin
359
- @constructor_params = Array.new
360
- @indent = 0
361
- ios = doc_pathname.open("w")
362
- api_doc = ApiDoc.new(api_name)
363
- api_doc.process_doc(ios)
364
- ensure
365
- ios.close
366
- end
367
- #end
368
- end
369
- end
370
-
371
- def get_class_hash()
372
- class_hash = Hash.new
373
- yardoc = File.join(@rootpath, '.yardoc')
374
- Registry.load!(yardoc) # loads all objects into memory
375
- class_array = Registry.all(:class) # Array
376
- class_array.each do |cls|
377
- class_hash[cls.name.to_s] = cls
378
- end
379
- class_hash
380
- end
381
-
382
- end
383
-
384
- apis = Array.new
385
- apis << 'FileInventory'
386
- apis << 'SignatureCatalog'
387
- apis << 'FileInventoryDifference'
388
- apis << 'VersionMetadata'
389
- apis << 'Serializable'
390
- apis << 'StorageObject'
391
- apis << 'StorageRepository'
392
- apis << 'DorMetadata'
393
-
394
- sg = DocGenerator.new(File.expand_path(File.join(File.dirname(__FILE__), '..', '..')))
395
- sg.generate_docs(apis)
@@ -1,409 +0,0 @@
1
- require 'rubygems'
2
- require 'pathname'
3
- require 'yard'
4
- include YARD
5
-
6
- class String
7
- def snake_case
8
- self.gsub(/::/, '/').
9
- gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').
10
- gsub(/([a-z\d])([A-Z])/, '\1_\2').
11
- tr("-", "_").
12
- downcase
13
- end
14
- end
15
-
16
- class Symbol
17
- def snake_case
18
- self.to_s.snake_case
19
- end
20
- end
21
-
22
- class SpecGenerator
23
-
24
- def initialize(rootpath)
25
- @rootpath = rootpath
26
- @ios = $stdout
27
- end
28
-
29
- def generate_tests
30
- spec_pathname = Pathname(@rootpath).join('spec', 'temp', "temp_unit_tests")
31
- classes = get_classes
32
- classes.each do |cls|
33
- test_pathname = spec_pathname.join(cls.path.snake_case + "_spec.rb")
34
- test_pathname.parent.mkpath
35
- #puts test_pathname.to_s
36
- unless test_pathname.exist?
37
- begin
38
- @constructor_params = Array.new
39
- @indent = 0
40
- @ios = test_pathname.open("w")
41
- process_class(cls)
42
- ensure
43
- @ios.close
44
- end
45
- end
46
- end
47
- end
48
-
49
- def get_classes()
50
- yardoc = File.join(@rootpath, '.yardoc')
51
- Registry.load!(yardoc) # loads all objects into memory
52
- Registry.all(:class) # Array
53
- end
54
-
55
- # @param string [String]
56
- def output(string)
57
- @ios.puts " "*@indent + string
58
- end
59
-
60
- # @param cls [CodeObjects::ClassObject]
61
- def process_class(cls)
62
- output %q{require File.join('..', '..','spec_helper')}
63
- output ""
64
- output "# Unit tests for class {#{cls.path}}"
65
- output "describe '#{cls.path}' do"
66
- @indent += 1
67
- mhash = categorize_members(cls)
68
- if mhash[:class_attributes].size > 0
69
- process_attributes(cls, :class, mhash[:class_attributes])
70
- end
71
- if mhash[:class_methods].size > 0
72
- process_methods(cls, :class, mhash)
73
- end
74
-
75
- process_constructor(cls, mhash)
76
-
77
- if mhash[:instance_attributes].size > 0
78
- process_attributes(cls, :instance, mhash[:instance_attributes])
79
- end
80
- if mhash[:instance_methods].size > 0
81
- process_methods(cls, :instance, mhash)
82
- end
83
-
84
- @indent -= 1
85
- output ""
86
- output "end"
87
- end
88
-
89
- # @param cls [CodeObjects::ClassObject]
90
- def categorize_members(cls)
91
- mhash = {
92
- :class_attributes => Hash.new,
93
- :instance_attributes => Hash.new,
94
- :class_methods => Array.new,
95
- :instance_methods => Array.new
96
- }
97
- cls.children.each do |member|
98
- attr_symbol = member.name.to_s.gsub(/=$/, '').to_sym
99
- if member.name == :initialize
100
- mhash[:constructor] = member
101
- elsif cls.class_attributes[attr_symbol]
102
- mhash[:class_attributes][attr_symbol] = cls.class_attributes[attr_symbol]
103
- elsif member.name.to_s[0..1] == '@@'
104
- #mhash[:class_attributes][attr_symbol] = cls.class_attributes[attr_symbol]
105
- elsif cls.instance_attributes[attr_symbol]
106
- mhash[:instance_attributes][attr_symbol] = cls.instance_attributes[attr_symbol]
107
- elsif not member.respond_to?(:scope)
108
- #puts 'huh?'
109
- elsif member.scope == :class
110
- mhash[:class_methods] << member
111
- elsif member.scope == :instance
112
- mhash[:instance_methods] << member
113
- end
114
- end
115
- mhash
116
- end
117
-
118
- def process_constructor(cls, mhash)
119
- output ""
120
- output "describe '=========================== CONSTRUCTOR ===========================' do"
121
-
122
- @indent += 1
123
- method = mhash[:constructor]
124
- if method.respond_to?(:docstring)
125
- @constructor_params = method.docstring.tags(:param)
126
-
127
- output ""
128
- output "# Unit test for constructor: {#{method.path}}"
129
- output "# Which returns an instance of: [#{cls.path}]"
130
- output "# For input parameters:"
131
- @constructor_params.each do |p|
132
- output "# * #{p.name} [#{p.types.join(', ')}] = #{p.text.gsub(/\n/, ' ')} "
133
- end
134
- output "specify '#{method.path}' do"
135
- @indent += 1
136
-
137
- cls_instance = cls.name.snake_case
138
- param_list = (@constructor_params.collect { |p| p.name }).join(', ')
139
-
140
- output " "
141
- output "# test initialization with required parameters (if any)"
142
- @constructor_params.each do |p|
143
- if p.name == 'opts'
144
- output "opts = {}"
145
- else
146
- output "#{p.name} = #{value_for(p.name, p.types[0])} "
147
- end
148
- end
149
- output "#{cls_instance} = #{cls.name}.new(#{param_list})"
150
- output "#{cls_instance}.should be_instance_of(#{cls.name})"
151
- @constructor_params.each do |p|
152
- unless p.name == 'opts'
153
- output "#{cls_instance}.#{p.name}.should == #{p.name}"
154
- end
155
- end
156
-
157
- arrays_and_hashes = attribute_arrays_and_hashes(mhash)
158
- if arrays_and_hashes.size > 0
159
- output " "
160
- output "# test initialization of arrays and hashes"
161
- arrays_and_hashes.each do |attr_name, type|
162
- output "#{cls_instance}.#{attr_name}.should be_kind_of(#{type})"
163
- end
164
- end
165
-
166
- @constructor_params.each do |p|
167
- if p.name == 'opts'
168
- output " "
169
- output "# test initialization with options hash"
170
- output "opts = Hash.new"
171
- attribute_name_type_pairs(mhash).each do |name, type|
172
- output "opts[:#{name}] = #{value_for(name, type)}"
173
- end
174
- output "#{cls_instance} = #{cls.name}.new(#{param_list})"
175
- attribute_name_type_pairs(mhash).each do |name, type|
176
- output "#{cls_instance}.#{name}.should == opts[:#{name}]"
177
- end
178
- end
179
- end
180
-
181
- output_source(method)
182
- @indent -= 1
183
-
184
- output "end"
185
-
186
- end
187
- @indent -= 1
188
- output ""
189
- output "end"
190
- end
191
-
192
- def process_attributes(cls, scope, attributes)
193
- output ""
194
- output "describe '=========================== #{scope.to_s.upcase} ATTRIBUTES ===========================' do"
195
- @indent += 1
196
-
197
- if scope == :instance
198
- output ""
199
- output "before(:all) do"
200
- @indent += 1
201
- @constructor_params.each do |p|
202
- if p.name == 'opts'
203
- output "opts = {}"
204
- else
205
- output "#{p.name} = #{value_for(p.name, p.types[0])} "
206
- end
207
- end
208
- cls_instance = cls.name.snake_case
209
- param_list = (@constructor_params.collect { |p| p.name }).join(', ')
210
- output "@#{cls_instance} = #{cls.name}.new(#{param_list})"
211
- @indent -= 1
212
- output "end"
213
- end
214
-
215
- attributes.values.each do |attribute|
216
- if attribute.nil?
217
- puts "huh?"
218
- end
219
- write = attribute[:write]
220
- read = attribute[:read]
221
- return_tag = read.docstring.tag(:return)
222
- return_type = return_tag.types[0]
223
- output ""
224
- output "# Unit test for attribute: {#{read.path}}"
225
- output "# Which stores: [#{return_type}] #{return_tag.text.gsub(/\n/, ' ')}"
226
- output "specify '#{read.path}' do"
227
-
228
- @indent += 1
229
- output "value = #{value_for(read.name, return_type)}"
230
- case scope
231
- when :class
232
- output "#{write.path} value"
233
- output "#{read.path}.should == value"
234
- when :instance
235
- output "@#{cls.name.snake_case}.#{write.name} value"
236
- output "@#{cls.name.snake_case}.#{read.name}.should == value"
237
- end
238
-
239
- output_source(write)
240
- output_source(read)
241
-
242
- @indent -= 1
243
- output "end"
244
- end
245
- @indent -= 1
246
-
247
- output ""
248
- output "end"
249
- end
250
-
251
- def process_methods(cls, scope, mhash)
252
- output ""
253
- output "describe '=========================== #{scope.to_s.upcase} METHODS ===========================' do"
254
- @indent += 1
255
-
256
- if scope == :instance
257
- output ""
258
- output "before(:each) do"
259
- @indent += 1
260
- @constructor_params.each do |p|
261
- if p.name == 'opts'
262
- output "@opts = {}"
263
- else
264
- output "@#{p.name} = #{value_for(p.name, p.types[0])} "
265
- end
266
- end
267
- cls_instance = cls.name.snake_case
268
- param_list = (@constructor_params.collect { |p| '@'+p.name }).join(', ')
269
- output "@#{cls_instance} = #{cls.name}.new(#{param_list})"
270
- output ""
271
- attribute_name_type_pairs(mhash).each do |name, type|
272
- output "@#{cls_instance}.#{name} = #{value_for(name, type)}"
273
- end
274
- @indent -= 1
275
- output "end"
276
- end
277
-
278
- case scope
279
- when :class
280
- methods =mhash[:class_methods]
281
- when :instance
282
- methods =mhash[:instance_methods]
283
- end
284
-
285
- methods.each do |method|
286
- begin
287
- return_tag = method.docstring.tag(:return)
288
- return_type = return_tag.types[0]
289
-
290
-
291
- params = method.docstring.tags(:param)
292
-
293
- output ""
294
- output "# Unit test for method: {#{method.path}}"
295
- output "# Which returns: [#{return_type}] #{return_tag.text.gsub(/\n/, ' ')}"
296
- if params.length > 0
297
- output "# For input parameters:"
298
- params.each do |p|
299
- output "# * #{p.name} [#{p.types.join(', ')}] = #{p.text.gsub(/\n/, ' ')} "
300
- end
301
- else
302
- output "# For input parameters: (None)"
303
- end
304
-
305
- output "specify '#{method.path}' do"
306
-
307
- @indent += 1
308
- params.each do |p|
309
- output "#{p.name} = #{value_for(p.name, p.types[0])} "
310
- end
311
- param_list = (params.collect { |p| p.name }).join(', ')
312
- if return_type == 'void'
313
- case scope
314
- when :class
315
- output "#{method.path}(#{param_list})"
316
- when :instance
317
- output "@#{cls.name.snake_case}.#{method.name}(#{param_list})"
318
- end
319
- else
320
- case scope
321
- when :class
322
- output "#{method.path}(#{param_list}).should == "
323
- when :instance
324
- output "@#{cls.name.snake_case}.#{method.name}(#{param_list}).should == "
325
- end
326
- end
327
-
328
- output_source(method)
329
- @indent -= 1
330
-
331
- output "end"
332
-
333
- rescue Exception => e
334
- raise "Error processing method: #{method.path} - " + e.message
335
- e.backtrace
336
- end
337
- end
338
- @indent -= 1
339
-
340
- output ""
341
- output "end"
342
- end
343
-
344
-
345
- def value_for(name, return_type)
346
- case return_type
347
- when 'Symbol'
348
- ":test_#{name}"
349
- when 'Integer'
350
- rand(100).to_s
351
- when 'String'
352
- "'Test #{name}'"
353
- when 'Boolean'
354
- "true"
355
- when 'Pathname'
356
- "Pathname.new('/test/#{name}')"
357
- when 'Time'
358
- "Time.now"
359
- else
360
- type = return_type.split(/[<>]/)
361
- if type.length > 1
362
- case type[0]
363
- when 'Array'
364
- "[#{value_for(name, type[1])}]"
365
- when 'Hash'
366
- key, value = type[1].split(/[,]/)
367
- "{#{value_for(name, key)} => #{value_for(name, value)}}"
368
- else
369
- "double(#{return_type}.name)"
370
- end
371
- else
372
- "double(#{return_type}.name)"
373
- end
374
- end
375
- end
376
-
377
- def attribute_arrays_and_hashes(mhash)
378
- arrays_and_hashes = Hash.new
379
- attribute_name_type_pairs(mhash).each do |name, type|
380
- if type.include?('Array')
381
- arrays_and_hashes[name] = 'Array'
382
- elsif type.include?('Hash')
383
- arrays_and_hashes[name] = 'Hash'
384
- end
385
- end
386
- arrays_and_hashes
387
- end
388
-
389
- def attribute_name_type_pairs(mhash)
390
- pairs = Hash.new
391
- mhash[:instance_attributes].values.each do |attribute|
392
- read = attribute[:read]
393
- return_tag = read.docstring.tag(:return)
394
- pairs[read.name] =return_tag.types[0]
395
- end
396
- pairs
397
- end
398
-
399
- def output_source(method)
400
- output " "
401
- lines = method.source.split(/\n/)
402
- lines.each { |line| output "# #{line}" }
403
- end
404
-
405
- end
406
-
407
-
408
- sg = SpecGenerator.new(File.expand_path(File.join(File.dirname(__FILE__), '..', '..')))
409
- sg.generate_tests
@@ -1,49 +0,0 @@
1
- rootpath = File.expand_path(File.join(File.dirname(__FILE__), '..'))
2
- puts rootpath
3
- lib = File.join(rootpath,'lib')
4
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
- require 'moab'
6
- #spec_helper = File.join(rootpath,'spec','spec_helper')
7
-
8
- class SpecGeneratorOld
9
-
10
- attr_accessor :c
11
- attr_accessor :members
12
-
13
- def initialize(c)
14
- @c=c
15
- #require camel_to_snake_case(c.name)
16
- i_members = c.instance_methods(include_super=false)
17
- i_setters = i_members.grep /[^=][=]$/
18
- i_getters = i_setters.collect { |s| s.gsub(/[=]$/,'') }
19
- @members = Hash.new
20
- @members['Instance Variables'] = (i_getters).sort
21
- @members['Instance Methods'] = (i_members - i_setters - i_getters).sort
22
- @members['Class Methods'] = c.methods(include_super=false).sort
23
- end
24
-
25
- def generate
26
- puts "require 'spec_helper'"
27
- puts ""
28
- puts "describe '#{@c.name}' do"
29
- puts ""
30
- ['Instance Variables','Instance Methods','Class Methods'].each do |type |
31
- puts " context '#{type}' do"
32
- puts ""
33
- @members[type].each do |member|
34
- puts " describe '##{member}' do"
35
- puts " pending"
36
- puts " end"
37
- puts ""
38
- end
39
- puts " end"
40
- puts ""
41
- end
42
- puts "end"
43
- end
44
-
45
- def camel_to_snake_case(camel)
46
- camel.gsub(/(.)([A-Z])/,'\1_\2').downcase
47
- end
48
-
49
- end