mss-sdk 1.0.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.
Files changed (131) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +9 -0
  3. data/LICENSE.txt +0 -0
  4. data/README.md +192 -0
  5. data/bin/mss-rb +178 -0
  6. data/ca-bundle.crt +3554 -0
  7. data/lib/mss/core/async_handle.rb +89 -0
  8. data/lib/mss/core/cacheable.rb +76 -0
  9. data/lib/mss/core/client.rb +786 -0
  10. data/lib/mss/core/collection/simple.rb +81 -0
  11. data/lib/mss/core/collection/with_limit_and_next_token.rb +70 -0
  12. data/lib/mss/core/collection/with_next_token.rb +96 -0
  13. data/lib/mss/core/collection.rb +262 -0
  14. data/lib/mss/core/configuration.rb +527 -0
  15. data/lib/mss/core/credential_providers.rb +653 -0
  16. data/lib/mss/core/data.rb +251 -0
  17. data/lib/mss/core/deprecations.rb +83 -0
  18. data/lib/mss/core/endpoints.rb +36 -0
  19. data/lib/mss/core/http/connection_pool.rb +374 -0
  20. data/lib/mss/core/http/curb_handler.rb +150 -0
  21. data/lib/mss/core/http/handler.rb +88 -0
  22. data/lib/mss/core/http/net_http_handler.rb +144 -0
  23. data/lib/mss/core/http/patch.rb +98 -0
  24. data/lib/mss/core/http/request.rb +258 -0
  25. data/lib/mss/core/http/response.rb +80 -0
  26. data/lib/mss/core/indifferent_hash.rb +87 -0
  27. data/lib/mss/core/inflection.rb +55 -0
  28. data/lib/mss/core/ini_parser.rb +41 -0
  29. data/lib/mss/core/json_client.rb +46 -0
  30. data/lib/mss/core/json_parser.rb +75 -0
  31. data/lib/mss/core/json_request_builder.rb +34 -0
  32. data/lib/mss/core/json_response_parser.rb +78 -0
  33. data/lib/mss/core/lazy_error_classes.rb +107 -0
  34. data/lib/mss/core/log_formatter.rb +426 -0
  35. data/lib/mss/core/managed_file.rb +31 -0
  36. data/lib/mss/core/meta_utils.rb +44 -0
  37. data/lib/mss/core/model.rb +61 -0
  38. data/lib/mss/core/naming.rb +29 -0
  39. data/lib/mss/core/option_grammar.rb +737 -0
  40. data/lib/mss/core/options/json_serializer.rb +81 -0
  41. data/lib/mss/core/options/validator.rb +154 -0
  42. data/lib/mss/core/options/xml_serializer.rb +117 -0
  43. data/lib/mss/core/page_result.rb +74 -0
  44. data/lib/mss/core/policy.rb +938 -0
  45. data/lib/mss/core/query_client.rb +40 -0
  46. data/lib/mss/core/query_error_parser.rb +23 -0
  47. data/lib/mss/core/query_request_builder.rb +46 -0
  48. data/lib/mss/core/query_response_parser.rb +34 -0
  49. data/lib/mss/core/region.rb +84 -0
  50. data/lib/mss/core/region_collection.rb +79 -0
  51. data/lib/mss/core/resource.rb +412 -0
  52. data/lib/mss/core/resource_cache.rb +39 -0
  53. data/lib/mss/core/response.rb +214 -0
  54. data/lib/mss/core/response_cache.rb +49 -0
  55. data/lib/mss/core/rest_error_parser.rb +23 -0
  56. data/lib/mss/core/rest_json_client.rb +39 -0
  57. data/lib/mss/core/rest_request_builder.rb +153 -0
  58. data/lib/mss/core/rest_response_parser.rb +65 -0
  59. data/lib/mss/core/rest_xml_client.rb +46 -0
  60. data/lib/mss/core/service_interface.rb +82 -0
  61. data/lib/mss/core/signers/base.rb +45 -0
  62. data/lib/mss/core/signers/cloud_front.rb +55 -0
  63. data/lib/mss/core/signers/s3.rb +158 -0
  64. data/lib/mss/core/signers/version_2.rb +71 -0
  65. data/lib/mss/core/signers/version_3.rb +85 -0
  66. data/lib/mss/core/signers/version_3_https.rb +60 -0
  67. data/lib/mss/core/signers/version_4/chunk_signed_stream.rb +190 -0
  68. data/lib/mss/core/signers/version_4.rb +227 -0
  69. data/lib/mss/core/uri_escape.rb +43 -0
  70. data/lib/mss/core/xml/frame.rb +245 -0
  71. data/lib/mss/core/xml/frame_stack.rb +84 -0
  72. data/lib/mss/core/xml/grammar.rb +306 -0
  73. data/lib/mss/core/xml/parser.rb +69 -0
  74. data/lib/mss/core/xml/root_frame.rb +64 -0
  75. data/lib/mss/core/xml/sax_handlers/libxml.rb +46 -0
  76. data/lib/mss/core/xml/sax_handlers/nokogiri.rb +55 -0
  77. data/lib/mss/core/xml/sax_handlers/ox.rb +40 -0
  78. data/lib/mss/core/xml/sax_handlers/rexml.rb +46 -0
  79. data/lib/mss/core/xml/stub.rb +122 -0
  80. data/lib/mss/core.rb +602 -0
  81. data/lib/mss/errors.rb +161 -0
  82. data/lib/mss/rails.rb +194 -0
  83. data/lib/mss/s3/access_control_list.rb +262 -0
  84. data/lib/mss/s3/acl_object.rb +263 -0
  85. data/lib/mss/s3/acl_options.rb +200 -0
  86. data/lib/mss/s3/bucket.rb +757 -0
  87. data/lib/mss/s3/bucket_collection.rb +161 -0
  88. data/lib/mss/s3/bucket_lifecycle_configuration.rb +472 -0
  89. data/lib/mss/s3/bucket_region_cache.rb +51 -0
  90. data/lib/mss/s3/bucket_tag_collection.rb +110 -0
  91. data/lib/mss/s3/bucket_version_collection.rb +78 -0
  92. data/lib/mss/s3/cipher_io.rb +119 -0
  93. data/lib/mss/s3/client/xml.rb +265 -0
  94. data/lib/mss/s3/client.rb +2076 -0
  95. data/lib/mss/s3/config.rb +60 -0
  96. data/lib/mss/s3/cors_rule.rb +107 -0
  97. data/lib/mss/s3/cors_rule_collection.rb +193 -0
  98. data/lib/mss/s3/data_options.rb +190 -0
  99. data/lib/mss/s3/encryption_utils.rb +145 -0
  100. data/lib/mss/s3/errors.rb +93 -0
  101. data/lib/mss/s3/multipart_upload.rb +353 -0
  102. data/lib/mss/s3/multipart_upload_collection.rb +75 -0
  103. data/lib/mss/s3/object_collection.rb +355 -0
  104. data/lib/mss/s3/object_metadata.rb +102 -0
  105. data/lib/mss/s3/object_upload_collection.rb +76 -0
  106. data/lib/mss/s3/object_version.rb +153 -0
  107. data/lib/mss/s3/object_version_collection.rb +88 -0
  108. data/lib/mss/s3/paginated_collection.rb +74 -0
  109. data/lib/mss/s3/policy.rb +73 -0
  110. data/lib/mss/s3/prefix_and_delimiter_collection.rb +46 -0
  111. data/lib/mss/s3/prefixed_collection.rb +84 -0
  112. data/lib/mss/s3/presign_v4.rb +135 -0
  113. data/lib/mss/s3/presigned_post.rb +574 -0
  114. data/lib/mss/s3/region_detection.rb +75 -0
  115. data/lib/mss/s3/request.rb +61 -0
  116. data/lib/mss/s3/s3_object.rb +1795 -0
  117. data/lib/mss/s3/tree/branch_node.rb +67 -0
  118. data/lib/mss/s3/tree/child_collection.rb +103 -0
  119. data/lib/mss/s3/tree/leaf_node.rb +93 -0
  120. data/lib/mss/s3/tree/node.rb +21 -0
  121. data/lib/mss/s3/tree/parent.rb +86 -0
  122. data/lib/mss/s3/tree.rb +115 -0
  123. data/lib/mss/s3/uploaded_part.rb +81 -0
  124. data/lib/mss/s3/uploaded_part_collection.rb +83 -0
  125. data/lib/mss/s3/website_configuration.rb +101 -0
  126. data/lib/mss/s3.rb +161 -0
  127. data/lib/mss/version.rb +16 -0
  128. data/lib/mss-sdk.rb +2 -0
  129. data/lib/mss.rb +14 -0
  130. data/rails/init.rb +14 -0
  131. metadata +201 -0
@@ -0,0 +1,263 @@
1
+ # Copyright 2011-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License"). You
4
+ # may not use this file except in compliance with the License. A copy of
5
+ # the License is located at
6
+ #
7
+ #
8
+ # or in the "license" file accompanying this file. This file is
9
+ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
10
+ # ANY KIND, either express or implied. See the License for the specific
11
+ # language governing permissions and limitations under the License.
12
+
13
+ require 'rexml/text'
14
+
15
+ module MSS
16
+ class S3
17
+
18
+ # Common methods for AccessControlList and related objects.
19
+ module ACLObject
20
+
21
+ # @api private
22
+ def initialize(opts = {}); end
23
+
24
+ # @api private
25
+ def body_xml; ""; end
26
+
27
+ # @api private
28
+ def stag
29
+ element_name
30
+ end
31
+
32
+ # @api private
33
+ def element_name
34
+ self.class.name[/::([^:]*)$/, 1]
35
+ end
36
+
37
+ # Returns the XML representation of the object. Generally
38
+ # you'll want to call this on an AccessControlList object,
39
+ # which will yield an XML representation of the ACL that you
40
+ # can send to S3.
41
+ def to_s
42
+ if body_xml.empty?
43
+ "<#{stag}/>"
44
+ else
45
+ "<#{stag}>#{body_xml}</#{element_name}>"
46
+ end
47
+ end
48
+
49
+ # (see #to_s)
50
+ def to_xml
51
+ to_s
52
+ end
53
+
54
+ # Returns true if and only if this object is valid according
55
+ # to S3's published ACL schema. In particular, this will
56
+ # check that all required attributes are provided and that
57
+ # they are of the correct type.
58
+ def valid?
59
+ validate!
60
+ rescue => e
61
+ false
62
+ else
63
+ true
64
+ end
65
+
66
+ # Raises an exception unless this object is valid according to
67
+ # S3's published ACL schema.
68
+ # @see #valid?
69
+ def validate!; end
70
+
71
+ # @api private
72
+ def validate_input(name, value, context = nil)
73
+ send("validate_#{name}_input!", value, context)
74
+ end
75
+
76
+ # @api private
77
+ module ClassMethods
78
+
79
+ def string_attr(element_name, options = {})
80
+ method_name = options[:method_name] ||
81
+ Core::Inflection.ruby_name(element_name)
82
+
83
+ attr_accessor(method_name)
84
+ setter_option(method_name)
85
+ string_input_validator(method_name)
86
+ validate_string(method_name) if options[:required]
87
+ body_xml_string_content(element_name, method_name)
88
+ end
89
+
90
+ def object_attr(klass, options = {})
91
+ base_name = klass.name[/::([^:]*)$/, 1]
92
+ method_name = Core::Inflection.ruby_name(base_name)
93
+ cast = options[:cast] || Hash
94
+
95
+ attr_reader(method_name)
96
+ setter_option(method_name)
97
+ object_setter(klass, method_name, cast)
98
+ object_input_validator(klass, base_name, method_name, cast)
99
+ validate_object(method_name) if options[:required]
100
+ body_xml_content(method_name)
101
+ end
102
+
103
+ def object_list_attr(list_element, klass, options = {})
104
+ base_name = klass.name[/::([^:]*)$/, 1]
105
+ method_name = Core::Inflection.ruby_name(options[:method_name].to_s || list_element)
106
+ default_value = nil
107
+ default_value = [] if options[:required]
108
+
109
+ attr_reader(method_name)
110
+ setter_option(method_name) { [] if options[:required] }
111
+ object_list_setter(klass, method_name)
112
+ object_list_input_validator(klass, base_name, method_name)
113
+ validate_list(method_name)
114
+ body_xml_list_content(list_element, method_name)
115
+ end
116
+
117
+ def setter_option(method_name)
118
+ Core::MetaUtils.class_extend_method(self, :initialize) do |*args|
119
+ opts = args.last || {}
120
+ instance_variable_set("@#{method_name}", yield) if block_given?
121
+ key = method_name.to_sym
122
+
123
+ if opts.has_key?(key)
124
+ value = opts[key]
125
+ validate_input(method_name, value, "for #{method_name} option")
126
+ self.send("#{method_name}=", value)
127
+ end
128
+ super(opts)
129
+ end
130
+ end
131
+
132
+ def string_input_validator(method_name)
133
+ input_validator(method_name) do |value, context|
134
+ raise ArgumentError.new("expected string"+context) unless
135
+ value.respond_to?(:to_str)
136
+ end
137
+ end
138
+
139
+ def object_input_validator(klass, base_name, method_name, cast)
140
+ input_validator(method_name) do |value, context|
141
+ if value.kind_of?(cast)
142
+ klass.new(value).validate!
143
+ else
144
+ raise ArgumentError.new("expected #{base_name} object or hash"+context) unless
145
+ value.nil? or value.kind_of? klass
146
+ end
147
+ end
148
+ end
149
+
150
+ def object_list_input_validator(klass, base_name, method_name)
151
+ input_validator(method_name) do |value, context|
152
+ raise ArgumentError.new("expected array"+context) unless value.kind_of?(Array)
153
+ value.each do |member|
154
+ if member.kind_of?(Hash)
155
+ klass.new(member).validate!
156
+ else
157
+ raise ArgumentError.new("expected array#{context} "+
158
+ "to contain #{base_name} objects "+
159
+ "or hashes") unless
160
+ member.kind_of? klass
161
+ end
162
+ end
163
+ end
164
+ end
165
+
166
+ def input_validator(method_name, &blk)
167
+ validator = "__validator__#{blk.object_id}"
168
+ Core::MetaUtils.class_extend_method(self, validator, &blk)
169
+ Core::MetaUtils.class_extend_method(self, "validate_#{method_name}_input!") do |*args|
170
+ (value, context) = args
171
+ context = " "+context if context
172
+ context ||= ""
173
+ send(validator, value, context)
174
+ end
175
+ end
176
+
177
+ def object_setter(klass, method_name, cast)
178
+ define_method("#{method_name}=") do |value|
179
+ validate_input(method_name, value)
180
+ if value.kind_of?(cast)
181
+ value = klass.new(value)
182
+ end
183
+ instance_variable_set("@#{method_name}", value)
184
+ end
185
+ end
186
+
187
+ def object_list_setter(klass, method_name)
188
+ define_method("#{method_name}=") do |value|
189
+ validate_input(method_name, value)
190
+ list = value.map do |member|
191
+ if member.kind_of?(Hash)
192
+ klass.new(member)
193
+ else
194
+ member
195
+ end
196
+ end
197
+ instance_variable_set("@#{method_name}", list)
198
+ end
199
+ end
200
+
201
+ def validate_string(method_name)
202
+ Core::MetaUtils.class_extend_method(self, :validate!) do
203
+ super()
204
+ raise "missing #{method_name}" unless send(method_name)
205
+ end
206
+ end
207
+
208
+ def validate_object(method_name)
209
+ Core::MetaUtils.class_extend_method(self, :validate!) do
210
+ super()
211
+ raise "missing #{method_name}" unless send(method_name)
212
+ send(method_name).validate!
213
+ end
214
+ end
215
+
216
+ def validate_list(method_name)
217
+ Core::MetaUtils.class_extend_method(self, :validate!) do
218
+ super()
219
+ raise "missing #{method_name}" unless send(method_name)
220
+ send(method_name).each { |member| member.validate! }
221
+ end
222
+ end
223
+
224
+ def body_xml_string_content(element_name, method_name)
225
+ add_xml_child(method_name) do |value|
226
+ normalized = REXML::Text.normalize(value.to_s)
227
+ "<#{element_name}>#{normalized}</#{element_name}>"
228
+ end
229
+ end
230
+
231
+ def body_xml_content(method_name)
232
+ add_xml_child(method_name) { |value| value.to_s }
233
+ end
234
+
235
+ def body_xml_list_content(list_element, method_name)
236
+ add_xml_child(method_name) do |list|
237
+ list_content = list.map { |member| member.to_s }.join
238
+ if list_content.empty?
239
+ "<#{list_element}/>"
240
+ else
241
+ "<#{list_element}>#{list_content}</#{list_element}>"
242
+ end
243
+ end
244
+ end
245
+
246
+ def add_xml_child(method_name)
247
+ Core::MetaUtils.class_extend_method(self, :body_xml) do
248
+ xml = super()
249
+ value = send(method_name)
250
+ xml += yield(value) if value
251
+ xml
252
+ end
253
+ end
254
+
255
+ end
256
+
257
+ def self.included(m)
258
+ m.extend(ClassMethods)
259
+ end
260
+
261
+ end
262
+ end
263
+ end
@@ -0,0 +1,200 @@
1
+ # Copyright 2011-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License"). You
4
+ # may not use this file except in compliance with the License. A copy of
5
+ # the License is located at
6
+ #
7
+ #
8
+ # or in the "license" file accompanying this file. This file is
9
+ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
10
+ # ANY KIND, either express or implied. See the License for the specific
11
+ # language governing permissions and limitations under the License.
12
+
13
+ require 'rexml/document'
14
+
15
+ module MSS
16
+ class S3
17
+
18
+ # Provides a method to {Bucket} and {S3Object} that parses a wide
19
+ # range of ACL options.
20
+ # @api private
21
+ module ACLOptions
22
+
23
+ protected
24
+
25
+ # @param [Symbol,String,Hash,AccessControlList] acl Accepts an ACL
26
+ # description in one of the following formats:
27
+ #
28
+ # ==== Canned ACL
29
+ #
30
+ # S3 supports a number of canned ACLs for buckets and
31
+ # objects. These include:
32
+ #
33
+ # * `:private`
34
+ # * `:public_read`
35
+ # * `:public_read_write`
36
+ # * `:authenticated_read`
37
+ # * `:bucket_owner_read` (object-only)
38
+ # * `:bucket_owner_full_control` (object-only)
39
+ # * `:log_delivery_write` (bucket-only)
40
+ #
41
+ # Here is an example of providing a canned ACL to a bucket:
42
+ #
43
+ # s3.buckets['bucket-name'].acl = :public_read
44
+ #
45
+ # ==== ACL Grant Hash
46
+ #
47
+ # You can provide a hash of grants. The hash is composed of grants (keys)
48
+ # and grantees (values). Accepted grant keys are:
49
+ #
50
+ # * `:grant_read`
51
+ # * `:grant_write`
52
+ # * `:grant_read_acp`
53
+ # * `:grant_write_acp`
54
+ # * `:grant_full_control`
55
+ #
56
+ # Grantee strings (values) should be formatted like some of the
57
+ # following examples:
58
+ #
59
+ # id="8a6925ce4adf588a4532142d3f74dd8c71fa124b1ddee97f21c32aa379004fef"
60
+ # uri="http://acs.amazonmss.com/groups/global/AllUsers"
61
+ #
62
+ # You can provide a comma delimited list of multiple grantees in a single
63
+ # string. Please note the use of quotes inside the grantee string.
64
+ # Here is a simple example:
65
+ #
66
+ # { :grant_full_control => "emailAddress=\"foo@bar.com\", id=\"abc..mno\"" }
67
+ #
68
+ # See the S3 API documentation for more information on formatting
69
+ # grants.
70
+ #
71
+ # ==== AcessControlList Object
72
+ #
73
+ # You can build an ACL using the {AccessControlList} class and
74
+ # pass this object.
75
+ #
76
+ # acl = MSS::S3::AccessControlList.new
77
+ # acl.grant(:full_control).to(:canonical_user_id => "8a6...fef")
78
+ # acl #=> this is acceptible
79
+ #
80
+ # ==== ACL XML String
81
+ #
82
+ # Lastly you can build your own ACL XML document and pass it as a string.
83
+ #
84
+ # <<-XML
85
+ # <AccessControlPolicy xmlns="http://s3.amazonmss.com/doc/2006-03-01/">
86
+ # <Owner>
87
+ # <ID>8a6...fef</ID>
88
+ # <DisplayName>owner-display-name</DisplayName>
89
+ # </Owner>
90
+ # <AccessControlList>
91
+ # <Grant>
92
+ # <Grantee xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="Canonical User">
93
+ # <ID>8a6...fef</ID>
94
+ # <DisplayName>owner-display-name</DisplayName>
95
+ # </Grantee>
96
+ # <Permission>FULL_CONTROL</Permission>
97
+ # </Grant>
98
+ # </AccessControlList>
99
+ # </AccessControlPolicy>
100
+ # XML
101
+ #
102
+ # @return [Hash] Returns a hash of options suitable for
103
+ # passing to {Client#put_bucket_acl} and {Client#put_object_acl}
104
+ # with a mixture of ACL options.
105
+ #
106
+ def acl_options acl
107
+ case acl
108
+ when Symbol
109
+ { :acl => acl.to_s.tr('_', '-') }
110
+ when String
111
+ # Strings are either access control policies (xml strings)
112
+ # or they are canned acls
113
+ xml?(acl) ?
114
+ { :access_control_policy => acl } :
115
+ { :acl => acl }
116
+ when AccessControlList
117
+ { :access_control_policy => acl.to_xml }
118
+ when Hash
119
+ # Hashes are either grant hashes or constructor args for an
120
+ # access control list (deprecated)
121
+ grant_hash?(acl) ?
122
+ format_grants(acl) :
123
+ { :access_control_policy => AccessControlList.new(acl).to_xml }
124
+ else
125
+ # failed to parse the acl option
126
+ msg = "expected a canned ACL, AccessControlList object, ACL "
127
+ "XML string or a grants hash"
128
+ raise ArgumentError, msg
129
+ end
130
+ end
131
+
132
+ # @param [Hash] acl_hash
133
+ # @return [Boolean] Retursn `true` if this hash is a hash of grants.
134
+ def grant_hash? acl_hash
135
+ grant_keys = [
136
+ :grant_read,
137
+ :grant_write,
138
+ :grant_read_acp,
139
+ :grant_write_acp,
140
+ :grant_full_control,
141
+ ]
142
+ acl_hash.keys.all?{|key| grant_keys.include?(key) }
143
+ end
144
+
145
+ # @param [String] acl_string
146
+ # @return [Boolean] Returns `true` if this string is an xml document.
147
+ def xml? acl_string
148
+ begin
149
+ REXML::Document.new(acl_string).has_elements?
150
+ rescue
151
+ false
152
+ end
153
+ end
154
+
155
+ # @param [Hash] acl_hash
156
+ # @return [Hash] Returns a hash of grant options suitable for
157
+ # passing to the various S3 client methods that accept ACL grants.
158
+ def format_grants acl_hash
159
+ grants = {}
160
+ acl_hash.each_pair do |grant,grantees|
161
+ grantees = [grantees] unless grantees.is_a?(Array)
162
+ grants[grant] = grantees.map{|g| format_grantee(g) }.join(', ')
163
+ end
164
+ grants
165
+ end
166
+
167
+ def format_grantee grantee
168
+ case grantee
169
+ when String then grantee
170
+ when Hash
171
+
172
+ if grantee.keys.count != 1
173
+ msg = "grantee hashes must have exactly 1 key"
174
+ raise ArgumentError, msg
175
+ end
176
+
177
+ # A granee hash looks like:
178
+ #
179
+ # { :id => 'abc...fec' }
180
+ # { :uri => 'http://abc.com/foo' }
181
+ #
182
+ # It needs to look like
183
+ #
184
+ # 'id="abc...fec"'
185
+ # 'uri="http://abc.com/foo"'
186
+ type, token = grantee.to_a.flatten
187
+ type = type.to_s.split('_').map{|part| ucfirst(part) }.join
188
+ "#{type[0,1].downcase}#{type[1..-1]}=\"#{token}\""
189
+ else
190
+ raise ArgumentError, "grantees must be a string or a hash"
191
+ end
192
+ end
193
+
194
+ def ucfirst str
195
+ str[0,1].upcase + str[1..-1]
196
+ end
197
+
198
+ end
199
+ end
200
+ end