treequel 1.2.2 → 1.3.0pre384
Sign up to get free protection for your applications and to get access to all the features.
- data.tar.gz.sig +0 -0
- data/ChangeLog +3374 -0
- data/History.md +39 -0
- data/LICENSE +27 -0
- data/README.md +25 -2
- data/Rakefile +64 -29
- data/bin/treequel +16 -25
- data/bin/treewhat +318 -0
- data/lib/treequel.rb +13 -3
- data/lib/treequel/behavior/control.rb +40 -0
- data/lib/treequel/branch.rb +56 -28
- data/lib/treequel/branchset.rb +10 -2
- data/lib/treequel/controls/pagedresults.rb +4 -2
- data/lib/treequel/directory.rb +40 -50
- data/lib/treequel/exceptions.rb +18 -0
- data/lib/treequel/mixins.rb +44 -14
- data/lib/treequel/model.rb +338 -21
- data/lib/treequel/model/errors.rb +79 -0
- data/lib/treequel/model/objectclass.rb +26 -2
- data/lib/treequel/model/schemavalidations.rb +69 -0
- data/lib/treequel/monkeypatches.rb +99 -17
- data/lib/treequel/schema.rb +19 -5
- data/spec/lib/constants.rb +20 -2
- data/spec/lib/helpers.rb +25 -8
- data/spec/treequel/branch_spec.rb +73 -10
- data/spec/treequel/controls/contentsync_spec.rb +2 -11
- data/spec/treequel/controls/pagedresults_spec.rb +25 -9
- data/spec/treequel/controls/sortedresults_spec.rb +8 -10
- data/spec/treequel/directory_spec.rb +74 -63
- data/spec/treequel/model/errors_spec.rb +77 -0
- data/spec/treequel/model/objectclass_spec.rb +107 -35
- data/spec/treequel/model/schemavalidations_spec.rb +112 -0
- data/spec/treequel/model_spec.rb +294 -81
- data/spec/treequel/monkeypatches_spec.rb +49 -3
- metadata +28 -16
- metadata.gz.sig +0 -0
- data/spec/lib/control_behavior.rb +0 -47
@@ -0,0 +1,79 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# coding: utf-8
|
3
|
+
|
4
|
+
require 'treequel'
|
5
|
+
require 'treequel/model'
|
6
|
+
require 'treequel/mixins'
|
7
|
+
require 'treequel/constants'
|
8
|
+
|
9
|
+
|
10
|
+
# Mixin that provides Treequel::Model characteristics to a mixin module.
|
11
|
+
#
|
12
|
+
# The ideas and a large portion of the implementation of this class is borrowed from
|
13
|
+
# Sequel under the following license terms:
|
14
|
+
#
|
15
|
+
# Copyright (c) 2007-2008 Sharon Rosner
|
16
|
+
# Copyright (c) 2008-2010 Jeremy Evans
|
17
|
+
#
|
18
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
19
|
+
# of this software and associated documentation files (the "Software"), to
|
20
|
+
# deal in the Software without restriction, including without limitation the
|
21
|
+
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
22
|
+
# sell copies of the Software, and to permit persons to whom the Software is
|
23
|
+
# furnished to do so, subject to the following conditions:
|
24
|
+
#
|
25
|
+
# The above copyright notice and this permission notice shall be included in
|
26
|
+
# all copies or substantial portions of the Software.
|
27
|
+
#
|
28
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
29
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
30
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
31
|
+
# THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
32
|
+
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
33
|
+
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
34
|
+
#
|
35
|
+
class Treequel::Model::Errors < ::Hash
|
36
|
+
include Treequel::HashUtilities,
|
37
|
+
Treequel::Loggable
|
38
|
+
|
39
|
+
# The word to use between attributes in error messages
|
40
|
+
ATTRIBUTE_CONJUNCTION = ' and '
|
41
|
+
|
42
|
+
|
43
|
+
### Set the initializer block to auto-create Array values.
|
44
|
+
def initialize( *args )
|
45
|
+
block = lambda {|h,k| h[k] = [] }
|
46
|
+
super( *args, &block )
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
### Adds an error for the given +attribute+.
|
51
|
+
### @param [Symbol, #to_sym] attribute the attribute with an error
|
52
|
+
### @param [String] message the description of the error condition
|
53
|
+
def add( attribute, message )
|
54
|
+
self[ attribute ] << message
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
### Get the number of errors that have been registered.
|
59
|
+
### @Return [Fixnum] the number of errors
|
60
|
+
def count
|
61
|
+
return self.values.inject( 0 ) {|num, val| num + val.length }
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
### Get an Array of messages describing errors which have occurred.
|
66
|
+
### @example
|
67
|
+
### errors.full_messages
|
68
|
+
### # => ['cn is not valid',
|
69
|
+
### # 'uid is not at least 2 letters']
|
70
|
+
def full_messages
|
71
|
+
return self.inject([]) do |full_messages, (attribute, messages)|
|
72
|
+
subject = Array( attribute ).join( ATTRIBUTE_CONJUNCTION )
|
73
|
+
messages.each {|part| full_messages << "#{subject} #{part}" }
|
74
|
+
full_messages
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
end # class Treequel::Model::Errors
|
79
|
+
|
@@ -9,12 +9,12 @@ require 'treequel/constants'
|
|
9
9
|
|
10
10
|
# Mixin that provides Treequel::Model characteristics to a mixin module.
|
11
11
|
module Treequel::Model::ObjectClass
|
12
|
+
include Treequel::HashUtilities
|
13
|
+
|
12
14
|
|
13
15
|
### Extension callback -- add data structures to the extending +mod+.
|
14
16
|
### @param [Module] mod the mixin module to be extended
|
15
17
|
def self::extended( mod )
|
16
|
-
# mod.instance_variable_set( :@model_directory, nil )
|
17
|
-
# mod.instance_variable_set( :@model_bases, [] )
|
18
18
|
mod.instance_variable_set( :@model_class, Treequel::Model )
|
19
19
|
mod.instance_variable_set( :@model_objectclasses, [] )
|
20
20
|
mod.instance_variable_set( :@model_bases, [] )
|
@@ -75,6 +75,30 @@ module Treequel::Model::ObjectClass
|
|
75
75
|
end
|
76
76
|
|
77
77
|
|
78
|
+
### Instantiate a new Treequel::Model object with given +dn+ and the objectclasses
|
79
|
+
### specified by the receiving module.
|
80
|
+
### @param [#to_s] dn the DN of the new model object
|
81
|
+
### @param [Hash] entryhash attributes to set on the new entry
|
82
|
+
def create( directory, dn, entryhash={} )
|
83
|
+
entryhash = stringify_keys( entryhash )
|
84
|
+
|
85
|
+
# Add the objectclasses from the mixin
|
86
|
+
entryhash['objectClass'] ||= []
|
87
|
+
entryhash['objectClass'].collect!( &:to_s )
|
88
|
+
entryhash['objectClass'] |= self.model_objectclasses.map( &:to_s )
|
89
|
+
|
90
|
+
# Add all the attribute pairs from the RDN bit of the DN to the entry
|
91
|
+
rdn_pair, _ = dn.split( /\s*,\s*/, 2 )
|
92
|
+
rdn_pair.split( /\+/ ).each do |attrpair|
|
93
|
+
k, v = attrpair.split( /\s*=\s*/ )
|
94
|
+
entryhash[ k ] ||= []
|
95
|
+
entryhash[ k ] << v
|
96
|
+
end
|
97
|
+
|
98
|
+
return self.model_class.new( directory, dn, entryhash )
|
99
|
+
end
|
100
|
+
|
101
|
+
|
78
102
|
### Return a Branchset (or BranchCollection if the receiver has more than one
|
79
103
|
### base) that can be used to search the given +directory+ for entries to which
|
80
104
|
### the receiver applies.
|
@@ -0,0 +1,69 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#encoding: utf-8
|
3
|
+
|
4
|
+
require 'treequel/model'
|
5
|
+
|
6
|
+
|
7
|
+
# A collection of schema-based validations for LDAP model objects.
|
8
|
+
module Treequel::Model::SchemaValidations
|
9
|
+
|
10
|
+
### Entrypoint -- run all the validations, adding any errors to the
|
11
|
+
### object's #error collector.
|
12
|
+
def validate( options={} )
|
13
|
+
return unless options[:with_schema]
|
14
|
+
|
15
|
+
self.validate_must_attributes
|
16
|
+
self.validate_may_attributes
|
17
|
+
self.validate_attribute_syntax
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
### Validate that all attributes that MUST be included according to the entry's
|
22
|
+
### objectClasses have at least one value.
|
23
|
+
def validate_must_attributes
|
24
|
+
self.must_attribute_types.each do |attrtype|
|
25
|
+
oid = attrtype.name
|
26
|
+
if attrtype.single?
|
27
|
+
self.errors.add( oid, "MUST have a value" ) unless self[ oid ]
|
28
|
+
else
|
29
|
+
self.errors.add( oid, "MUST have at least one value" ) if self[ oid ].empty?
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
### Validate that all attributes present in the entry are allowed by either a
|
36
|
+
### MUST or a MAY rule of one of its objectClasses.
|
37
|
+
def validate_may_attributes
|
38
|
+
hash = (self.entry || {} ).merge( @values )
|
39
|
+
attributes = hash.keys.map( &:to_sym ).uniq
|
40
|
+
valid_attributes = self.valid_attribute_oids
|
41
|
+
|
42
|
+
self.log.debug "Validating MAY attributes: %p against the list of valid OIDs: %p" %
|
43
|
+
[ attributes, valid_attributes ]
|
44
|
+
unknown_attributes = attributes - valid_attributes
|
45
|
+
unknown_attributes.each do |oid|
|
46
|
+
self.errors.add( oid, "is not allowed by entry's objectClasses" )
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
### Validate that the attribute values present in the entry are all valid according to
|
52
|
+
### the syntax rule for it.
|
53
|
+
def validate_attribute_syntax
|
54
|
+
@values.each do |attribute, values|
|
55
|
+
[ values ].flatten.each do |value|
|
56
|
+
begin
|
57
|
+
self.get_converted_attribute( attribute.to_sym, value )
|
58
|
+
rescue => err
|
59
|
+
self.log.error "validation for %p failed: %s: %s" %
|
60
|
+
[ attribute, err.class.name, err.message ]
|
61
|
+
attrtype = self.find_attribute_type( attribute )
|
62
|
+
self.errors.add( attribute, "isn't a valid %s value" % [attrtype.syntax.desc] )
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
end # module Treequel::Model::SchemaValidations
|
69
|
+
|
@@ -1,5 +1,10 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
+
require 'date'
|
4
|
+
|
5
|
+
require 'diff/lcs'
|
6
|
+
require 'diff/lcs/change'
|
7
|
+
|
3
8
|
require 'ldap'
|
4
9
|
require 'ldap/control'
|
5
10
|
|
@@ -57,26 +62,26 @@ module Treequel::TimeExtensions
|
|
57
62
|
if fraction_digits == 0
|
58
63
|
''
|
59
64
|
elsif fraction_digits <= 6
|
60
|
-
'.' + sprintf('%06d', usec)[0, fraction_digits]
|
65
|
+
'.' + sprintf('%06d', self.usec)[0, fraction_digits]
|
61
66
|
else
|
62
|
-
'.' + sprintf('%06d', usec) + '0' * (fraction_digits - 6)
|
67
|
+
'.' + sprintf('%06d', self.usec) + '0' * (fraction_digits - 6)
|
63
68
|
end
|
64
69
|
tz =
|
65
70
|
if self.utc?
|
66
71
|
'Z'
|
67
72
|
else
|
68
|
-
off = utc_offset
|
73
|
+
off = self.utc_offset
|
69
74
|
sign = off < 0 ? '-' : '+'
|
70
75
|
"%s%02d%02d" % [ sign, *(off.abs / 60).divmod(60) ]
|
71
76
|
end
|
72
77
|
|
73
78
|
return "%02d%02d%02d%02d%02d%02d%s%s" % [
|
74
|
-
year,
|
75
|
-
mon,
|
76
|
-
day,
|
77
|
-
hour,
|
78
|
-
min,
|
79
|
-
sec,
|
79
|
+
self.year,
|
80
|
+
self.mon,
|
81
|
+
self.day,
|
82
|
+
self.hour,
|
83
|
+
self.min,
|
84
|
+
self.sec,
|
80
85
|
fractional_seconds,
|
81
86
|
tz
|
82
87
|
]
|
@@ -87,21 +92,21 @@ module Treequel::TimeExtensions
|
|
87
92
|
### (UTC Time)
|
88
93
|
def ldap_utc
|
89
94
|
tz =
|
90
|
-
if utc?
|
95
|
+
if self.utc?
|
91
96
|
'Z'
|
92
97
|
else
|
93
|
-
off = utc_offset
|
98
|
+
off = self.utc_offset
|
94
99
|
sign = off < 0 ? '-' : '+'
|
95
100
|
"%s%02d%02d" % [ sign, *(off.abs / 60).divmod(60) ]
|
96
101
|
end
|
97
102
|
|
98
103
|
return "%02d%02d%02d%02d%02d%02d%s" % [
|
99
|
-
year.divmod(100).last,
|
100
|
-
mon,
|
101
|
-
day,
|
102
|
-
hour,
|
103
|
-
min,
|
104
|
-
sec,
|
104
|
+
self.year.divmod(100).last,
|
105
|
+
self.mon,
|
106
|
+
self.day,
|
107
|
+
self.hour,
|
108
|
+
self.min,
|
109
|
+
self.sec,
|
105
110
|
tz
|
106
111
|
]
|
107
112
|
end
|
@@ -113,3 +118,80 @@ class Time
|
|
113
118
|
end
|
114
119
|
|
115
120
|
|
121
|
+
### Extensions to the Date class to add LDAP (RFC4517) Generalized Time syntax
|
122
|
+
module Treequel::DateExtensions
|
123
|
+
|
124
|
+
### Return +self+ as a String formatted as specified in RFC4517
|
125
|
+
### (LDAP Generalized Time).
|
126
|
+
def ldap_generalized( fraction_digits=0 )
|
127
|
+
fractional_seconds =
|
128
|
+
if fraction_digits == 0
|
129
|
+
''
|
130
|
+
else
|
131
|
+
'.' + ('0' * fraction_digits)
|
132
|
+
end
|
133
|
+
|
134
|
+
off = Time.now.utc_offset
|
135
|
+
sign = off < 0 ? '-' : '+'
|
136
|
+
tz = "%s%02d%02d" % [ sign, *(off.abs / 60).divmod(60) ]
|
137
|
+
|
138
|
+
return "%02d%02d%02d%02d%02d%02d%s%s" % [
|
139
|
+
self.year,
|
140
|
+
self.mon,
|
141
|
+
self.day,
|
142
|
+
0,
|
143
|
+
0,
|
144
|
+
1,
|
145
|
+
fractional_seconds,
|
146
|
+
tz
|
147
|
+
]
|
148
|
+
|
149
|
+
end
|
150
|
+
|
151
|
+
### Returns +self+ as a String formatted as specified in RFC4517
|
152
|
+
### (UTC Time)
|
153
|
+
def ldap_utc
|
154
|
+
off = Time.now.utc_offset
|
155
|
+
sign = off < 0 ? '-' : '+'
|
156
|
+
tz = "%s%02d%02d" % [ sign, *(off.abs / 60).divmod(60) ]
|
157
|
+
|
158
|
+
return "%02d%02d%02d%02d%02d%02d%s" % [
|
159
|
+
self.year.divmod(100).last,
|
160
|
+
self.mon,
|
161
|
+
self.day,
|
162
|
+
0,
|
163
|
+
0,
|
164
|
+
1,
|
165
|
+
tz
|
166
|
+
]
|
167
|
+
end
|
168
|
+
|
169
|
+
end # module Treequel::TimeExtensions
|
170
|
+
|
171
|
+
class Date
|
172
|
+
include Treequel::DateExtensions
|
173
|
+
end
|
174
|
+
|
175
|
+
|
176
|
+
### These three predicates use the wrong instance variable in the library.
|
177
|
+
### :TODO: Submit a patch!
|
178
|
+
module Treequel::DiffLCSChangeTypeTestFixes
|
179
|
+
|
180
|
+
def changed?
|
181
|
+
@action == '!'
|
182
|
+
end
|
183
|
+
|
184
|
+
def finished_a?
|
185
|
+
@action == '>'
|
186
|
+
end
|
187
|
+
|
188
|
+
def finished_b?
|
189
|
+
@action == '<'
|
190
|
+
end
|
191
|
+
|
192
|
+
end
|
193
|
+
|
194
|
+
class Diff::LCS::ContextChange
|
195
|
+
include Treequel::DiffLCSChangeTypeTestFixes
|
196
|
+
end
|
197
|
+
|
data/lib/treequel/schema.rb
CHANGED
@@ -209,17 +209,21 @@ class Treequel::Schema
|
|
209
209
|
end
|
210
210
|
|
211
211
|
|
212
|
+
### Return the schema as a human-readable english string.
|
213
|
+
def to_s
|
214
|
+
parts = [ "Schema:" ]
|
215
|
+
parts << self.ivar_descriptions.collect {|desc| ' ' + desc }
|
216
|
+
return parts.join( $/ )
|
217
|
+
end
|
218
|
+
|
219
|
+
|
212
220
|
### Return a human-readable representation of the object suitable for debugging.
|
213
221
|
### @return [String]
|
214
222
|
def inspect
|
215
|
-
ivar_descs = self.instance_variables.sort.collect do |ivar|
|
216
|
-
len = self.instance_variable_get( ivar ).length
|
217
|
-
"%d %s" % [ len, ivar.to_s.gsub(/_/, ' ')[1..-1] ]
|
218
|
-
end
|
219
223
|
return %{#<%s:0x%0x %s>} % [
|
220
224
|
self.class.name,
|
221
225
|
self.object_id / 2,
|
222
|
-
|
226
|
+
self.ivar_descriptions.join( ', ' ),
|
223
227
|
]
|
224
228
|
end
|
225
229
|
|
@@ -308,5 +312,15 @@ class Treequel::Schema
|
|
308
312
|
end
|
309
313
|
|
310
314
|
|
315
|
+
### Return descriptions of the schema's artifacts.
|
316
|
+
### @return [Array<String>] the descriptions of the schema's artifacts, and how many of each
|
317
|
+
### it has.
|
318
|
+
def ivar_descriptions
|
319
|
+
self.instance_variables.sort.collect do |ivar|
|
320
|
+
len = self.instance_variable_get( ivar ).length
|
321
|
+
"%d %s" % [ len, ivar.to_s.gsub(/_/, ' ')[1..-1] ]
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
311
325
|
end # class Treequel::Schema
|
312
326
|
|
data/spec/lib/constants.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
+
require 'yaml'
|
3
4
|
require 'ldap'
|
4
5
|
require 'treequel'
|
5
6
|
|
@@ -40,7 +41,8 @@ module Treequel::TestConstants # :nodoc:all
|
|
40
41
|
"2.16.840.1.113730.3.4.2", "1.3.6.1.4.1.4203.1.10.1",
|
41
42
|
"1.2.840.113556.1.4.319", "1.2.826.0.1.334810.2.3",
|
42
43
|
"1.2.826.0.1.3344810.2.3", "1.3.6.1.1.13.2",
|
43
|
-
"1.3.6.1.1.13.1", "1.3.6.1.1.12"
|
44
|
+
"1.3.6.1.1.13.1", "1.3.6.1.1.12",
|
45
|
+
"1.2.840.113556.1.4.473", "1.2.840.113556.1.4.474"
|
44
46
|
],
|
45
47
|
"supportedExtension" => [
|
46
48
|
"1.3.6.1.4.1.1466.20037", "1.3.6.1.4.1.4203.1.11.1",
|
@@ -50,6 +52,9 @@ module Treequel::TestConstants # :nodoc:all
|
|
50
52
|
}]
|
51
53
|
TEST_DSE.first.keys.each {|key| TEST_DSE.first[key].freeze }
|
52
54
|
|
55
|
+
SCHEMA_DUMPFILE = Pathname( __FILE__ ).dirname.parent + 'data' + 'schema.yml'
|
56
|
+
SCHEMAHASH = LDAP::Schema.new( YAML.load_file(SCHEMA_DUMPFILE) )
|
57
|
+
SCHEMA = Treequel::Schema.new( SCHEMAHASH )
|
53
58
|
|
54
59
|
TEST_HOSTS_DN_ATTR = 'ou'
|
55
60
|
TEST_HOSTS_DN_VALUE = 'Hosts'
|
@@ -82,7 +87,7 @@ module Treequel::TestConstants # :nodoc:all
|
|
82
87
|
TEST_PEOPLE_DN = "#{TEST_PEOPLE_RDN},#{TEST_BASE_DN}"
|
83
88
|
|
84
89
|
TEST_PERSON_DN_ATTR = 'uid'
|
85
|
-
TEST_PERSON_DN_VALUE = '
|
90
|
+
TEST_PERSON_DN_VALUE = 'jrandom'
|
86
91
|
TEST_PERSON_RDN = "#{TEST_PERSON_DN_ATTR}=#{TEST_PERSON_DN_VALUE}"
|
87
92
|
TEST_PERSON_DN = "#{TEST_PERSON_RDN},#{TEST_PEOPLE_DN}"
|
88
93
|
|
@@ -106,6 +111,19 @@ module Treequel::TestConstants # :nodoc:all
|
|
106
111
|
TEST_ROOM_RDN = "#{TEST_ROOM_DN_ATTR}=#{TEST_ROOM_DN_VALUE}"
|
107
112
|
TEST_ROOM_DN = "#{TEST_ROOM_RDN},#{TEST_ROOMS_DN}"
|
108
113
|
|
114
|
+
# Multivalue DN
|
115
|
+
TEST_HOST_MULTIVALUE_DN_ATTR1 = 'cn'
|
116
|
+
TEST_HOST_MULTIVALUE_DN_VALUE1 = 'honcho'
|
117
|
+
TEST_HOST_MULTIVALUE_DN_ATTR2 = 'l'
|
118
|
+
TEST_HOST_MULTIVALUE_DN_VALUE2 = 'sandiego'
|
119
|
+
TEST_HOST_MULTIVALUE_RDN = "%s=%s+%s=%s" % [
|
120
|
+
TEST_HOST_MULTIVALUE_DN_ATTR1,
|
121
|
+
TEST_HOST_MULTIVALUE_DN_VALUE1,
|
122
|
+
TEST_HOST_MULTIVALUE_DN_ATTR2,
|
123
|
+
TEST_HOST_MULTIVALUE_DN_VALUE2,
|
124
|
+
]
|
125
|
+
TEST_HOST_MULTIVALUE_DN = "#{TEST_HOST_MULTIVALUE_RDN},#{TEST_HOSTS_DN}"
|
126
|
+
|
109
127
|
constants.each do |cname|
|
110
128
|
const_get(cname).freeze
|
111
129
|
end
|