moab-versioning 2.1.0 → 2.2.0

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.
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