stormpath-sdk 0.2.0 → 0.3.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.
data/CHANGES.md ADDED
@@ -0,0 +1,31 @@
1
+ stormpath-sdk-ruby Changelog
2
+ ====================
3
+
4
+ Version 0.3.0
5
+ -------------
6
+
7
+ Released on August 31, 2012
8
+
9
+ - The properties method is now protected, only available for the resources hierarchy. The properties on the data store are now obtained via Stormpath::Resource::Resource's get_property_names and get_property functions.
10
+ - The 'inspect' and 'to_s' methods were overridden in Stormpath::Resource::Resource to keep some properties (like password) from being displayed.
11
+ - Logic to retain non-persisted properties was added (dirty properties).
12
+ - A resource's property can now be removed by setting it to nil.
13
+ - The Stormpath::Client::ClientApplicationBuilder class was added and implemented to produce a Stormpath::Client::ClientApplication from a single URL with the credentials on it.
14
+
15
+
16
+ Version 0.2.0
17
+ -------------
18
+
19
+ Released on August 20, 2012
20
+
21
+ - Result of the Application authentication was changed from Account to AuthenticationResult.
22
+ - The password verification's method name for token creation on the Application class was changed to 'send_password_reset_email'.
23
+ - The 'verify_password_reset_token' method on the Application class now returns an Account instead of a PasswordResetToken.
24
+ - The API RDOC was updated for the previously modified implementations on the Application class.
25
+
26
+ Version 0.1.0
27
+ -------------
28
+
29
+ Released on July 27, 2012
30
+
31
+ - First release of the Stormpath Ruby SDK where all of the features available on the REST API by the release date were implemented.
data/README.md CHANGED
@@ -13,7 +13,10 @@ https://github.com/stormpath/stormpath-sdk-ruby/wiki
13
13
 
14
14
  Via rubygems.org:
15
15
 
16
+ ```
16
17
  $ gem install stormpath-sdk
18
+ ```
19
+
17
20
  To build and install the development branch yourself from the latest source:
18
21
 
19
22
  ```
data/lib/stormpath-sdk.rb CHANGED
@@ -13,6 +13,8 @@ require "stormpath-sdk/client/api_key"
13
13
  require "stormpath-sdk/client/client"
14
14
  require "stormpath-sdk/util/hash"
15
15
  require "stormpath-sdk/client/client_builder"
16
+ require "stormpath-sdk/client/client_application"
17
+ require "stormpath-sdk/client/client_application_builder"
16
18
  require "stormpath-sdk/auth/username_password_request"
17
19
  require "stormpath-sdk/resource/status"
18
20
  require "stormpath-sdk/resource/utils"
@@ -21,7 +21,7 @@ module Stormpath
21
21
 
22
22
  attr_reader :host
23
23
 
24
- def initialize username, password, host
24
+ def initialize username, password, host = nil
25
25
  @username = username
26
26
  @password = (password != nil and password.length > 0) ? password.chars.to_a : "".chars.to_a
27
27
  @host = host
@@ -0,0 +1,38 @@
1
+ #
2
+ # Copyright 2012 Stormpath, Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ module Stormpath
17
+
18
+ module Client
19
+
20
+ # A ClientApplication is a simple wrapper around a {@link Stormpath::Client::Client} and
21
+ # {@link Stormpath::Resource::Application} instance, returned from
22
+ # the {@code ClientApplicationBuilder}.{@link Stormpath::Client::ClientApplicationBuilder#build_application}
23
+ # method.
24
+ # @since 0.3.0
25
+ class ClientApplication
26
+
27
+ attr_reader :client, :application
28
+
29
+ def initialize client, application
30
+ @client = client
31
+ @application = application
32
+ end
33
+
34
+ end
35
+
36
+ end
37
+
38
+ end
@@ -0,0 +1,351 @@
1
+ #
2
+ # Copyright 2012 Stormpath, Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ module Stormpath
17
+
18
+ module Client
19
+
20
+ ##
21
+ # A <a href="http://en.wikipedia.org/wiki/Builder_pattern">Builder design pattern</a> implementation similar to
22
+ # the {@link Stormpath::Client::ClientBuilder}, but focused on single-application interaction with Stormpath.
23
+ # <h2>Description</h2>
24
+ # The {@code ClientBuilder} produces a {@link Stormpath::Client::Client} instance useful for interacting with any aspect
25
+ # of an entire Stormpath Tenant's data space. However, a software application may only be interested in its own
26
+ # functionality and not the entire Stormpath Tenant data space.
27
+ # <p/>
28
+ # The {@code ClientApplicationBuilder} provides a means to more easily acquiring a single
29
+ # {@link Stormpath::Resource::Application} instance. From this {@code Application} instance, everything a particular
30
+ # Application needs to perform can be based off of this instance and the wider-scoped concerns of an entire Tenant can be ignored.
31
+ # <h2>Default Usage</h2>
32
+ # <pre>
33
+ # # this can be a file disk or url location as well:
34
+ # location = "/home/jsmith/.stormpath/apiKey.yml"
35
+ #
36
+ # app_href = "https://api.stormpath.com/v1/applications/YOUR_APP_UID_HERE"
37
+ #
38
+ # application = new ClientApplicationBuilder.new.
39
+ # set_api_key_file_location(location).
40
+ # <b>set_application_href(app_href)</b>.
41
+ # build.
42
+ # application
43
+ # </pre>
44
+ # <p/>
45
+ # After acquiring the {@code Application} instance, you can interact with it to login accounts, reset passwords,
46
+ # etc.
47
+ # <h2>Service Provider Usage with only an Application URL</h2>
48
+ # Some hosting service providers (e.g. like <a href="http://www.heroku.com">Heroku</a>) do not allow easy access to
49
+ # a configuration file and therefore it might be difficult to reference an API Key File. If you cannot reference an
50
+ # API Key File via the {@code YAML file} or {@code YAML object} or {@code url}
51
+ # {@link ClientBuilder#set_api_key_file_location(String) resource locations}, the Application HREF URL must contain the
52
+ # API Key embedded as the <em><a href="http://en.wikipedia.org/wiki/URI_scheme">user info</a></em> portion of the
53
+ # URL. For example:
54
+ # <p/>
55
+ # <pre>
56
+ # https://<b>apiKeyId:apiKeySecret@</b>api.stormpath.com/v1/applications/YOUR_APP_UID_HERE
57
+ # </pre>
58
+ # <p/>
59
+ # Notice this is just a normal Application HREF url with the <b>apiKeyId:apiKeySecret@</b> part added in.
60
+ # <p/>
61
+ # Example usage:
62
+ # <pre>
63
+ # appHref = "https://<b>apiKeyId:apiKeySecret@</b>api.stormpath.com/v1/applications/YOUR_APP_UID_HERE";
64
+ #
65
+ # application = new ClientApplicationBuilder.new.
66
+ # <b>set_application_href(appHref)</b>.
67
+ # build.
68
+ # application
69
+ # </pre>
70
+ # <p/>
71
+ # <b>WARNING: ONLY use the embedded API Key technique if you do not have access to {@code YAML file} or
72
+ # {@code YAML object} or {@code url} {@link ClientApplicationBuilder#set_api_key_file_location(String) resource locations}</b>.
73
+ # File based API Key storage is a more secure technique than embedding the key in the URL itself. Also, again,
74
+ # NEVER share your API Key Secret with <em>anyone</em> (not even co-workers).
75
+ # Stormpath staff will never ask for your API Key Secret.
76
+ #
77
+ # @see #set_api_key_file_location(String)
78
+ # @see #set_application_href(String)
79
+ # @since 0.3.0
80
+ #
81
+ class ClientApplicationBuilder
82
+
83
+ include Stormpath::Client
84
+ include Stormpath::Util::Assert
85
+
86
+ DOUBLE_SLASH = "//"
87
+
88
+ def initialize client_builder = ClientBuilder.new
89
+
90
+ assert_kind_of ClientBuilder, client_builder, 'client_builder must be kind of Stormpath::Client::ClientBuilder'
91
+ @client_builder = client_builder
92
+
93
+ end
94
+
95
+ # Allows usage of a YAML loadable object (IO object or the result of invoking Object.to_yaml)
96
+ # instead of loading a YAML file via {@link #set_api_key_file_location apiKeyFileLocation} configuration.
97
+ # <p/>
98
+ # The YAML contents and property name overrides function the same as described in the
99
+ # {@link #set_api_key_file_location setApiKeyFileLocation} RDoc.
100
+ #
101
+ # @param properties the YAML object to use to load the API Key ID and Secret.
102
+ # @return this ClientApplicationBuilder instance for method chaining.
103
+ def set_api_key_properties properties
104
+
105
+ @client_builder.set_api_key_properties properties
106
+ self
107
+
108
+ end
109
+
110
+ # Creates an API Key YAML object based on the specified File instead of loading a YAML
111
+ # file via {@link #set_api_key_file_location apiKeyFileLocation} configuration. This file argument
112
+ # needs to be an IO instance.
113
+ # <p/>
114
+ # The constructed YAML contents and property name overrides function the same as described in the
115
+ # {@link #set_api_key_file_location setApiKeyFileLocation} RDoc.
116
+ # @param file the file to use to construct a YAML object.
117
+ # @return this ClientApplicationBuilder instance for method chaining.
118
+ def set_api_key_file file
119
+
120
+ @client_builder.set_api_key_file file
121
+ self
122
+
123
+ end
124
+
125
+ # Sets the location of the YAML file to load containing the API Key (Id and secret) used by the
126
+ # Client to communicate with the Stormpath REST API.
127
+ # <p/>
128
+ # You may load files from the filesystem, or URLs just specifying the file location.
129
+ # <h3>File Contents</h3>
130
+ # <p/>
131
+ # When the file is loaded, the following name/value pairs are expected to be present by default:
132
+ # <table>
133
+ # <tr>
134
+ # <th>Key</th>
135
+ # <th>Value</th>
136
+ # </tr>
137
+ # <tr>
138
+ # <td>apiKey.id</td>
139
+ # <td>An individual account's API Key ID</td>
140
+ # </tr>
141
+ # <tr>
142
+ # <td>apiKey.secret</td>
143
+ # <td>The API Key Secret (password) that verifies the paired API Key ID.</td>
144
+ # </tr>
145
+ # </table>
146
+ # <p/>
147
+ # Assuming you were using these default property names, your {@code ClientApplicationBuilder} usage might look like the
148
+ # following:
149
+ # <pre>
150
+ # location = "/home/jsmith/.stormpath/apiKey.yml";
151
+ #
152
+ # application_href = 'https://<b>apiKeyId:apiKeySecret@</b>api.stormpath.com/v1/applications/YOUR_APP_UID_HERE'
153
+ #
154
+ # application = ClientApplicationBuilder.new.
155
+ # set_application_href(application_href).
156
+ # set_api_key_file_location(location).
157
+ # build.
158
+ # application
159
+ # </pre>
160
+ # <h3>Custom Property Names</h3>
161
+ # If you want to control the property names used in the file, you may configure them via
162
+ # {@link #set_api_key_id_property_name(String) set_api_key_id_property_name} and
163
+ # {@link #set_api_key_secret_property_name(String) set_api_key_secret_property_name}.
164
+ # <p/>
165
+ # For example, if you had a {@code /home/jsmith/.stormpath/apiKey.yml} file with the following
166
+ # name/value pairs:
167
+ # <pre>
168
+ # myStormpathApiKeyId = 'foo'
169
+ # myStormpathApiKeySecret = 'mySuperSecretValue'
170
+ # </pre>
171
+ # Your {@code ClientApplicationBuilder} usage would look like the following:
172
+ # <pre>
173
+ # location = "/home/jsmith/.stormpath/apiKey.yml";
174
+ #
175
+ # application = ClientApplicationBuilder.new.
176
+ # set_api_key_file_location(location).
177
+ # set_api_key_id_property_name("myStormpathApiKeyId").
178
+ # set_api_key_secret_property_name("myStormpathApiKeySecret").
179
+ # set_application_href(application_href).
180
+ # build.
181
+ # application
182
+ # </pre>
183
+ #
184
+ # @param location the file or url location of the API Key {@code .yml} file to load when
185
+ # constructing the API Key to use for communicating with the Stormpath REST API.
186
+ #
187
+ # @return this ClientApplicationBuilder instance for method chaining.
188
+ #/
189
+ def set_api_key_file_location location
190
+
191
+ @client_builder.set_api_key_file_location location
192
+ self
193
+
194
+ end
195
+
196
+ # Sets the name used to query for the API Key ID from a YAML instance. That is:
197
+ # <pre>
198
+ # apiKeyId = yml.access(<b>api_key_id_property_name</b>)
199
+ # </pre>
200
+ #
201
+ # The Hash#access method searches through the provided path and returns the found value.
202
+ #
203
+ # The <b>api_key_id_property_name</b> key can be as deep as needed, as long as it comes
204
+ # in the exact path order.
205
+ # Example: Having the file 'apiKey.yml' with the following content:
206
+ #
207
+ # stormpath:
208
+ # apiKey:
209
+ # id: myStormpathApiKeyId
210
+ #
211
+ # The method should be called as follows:
212
+ #
213
+ # ClientApplicationBuilder#set_api_key_id_property_name('stormpath', 'apiKey', 'id')
214
+ #
215
+ # @param api_key_id_property_name the name used to query for the API Key ID from a YAML instance.
216
+ # @return this ClientApplicationBuilder instance for method chaining.
217
+ def set_api_key_id_property_name *api_key_id_property_name
218
+
219
+ @client_builder.set_api_key_id_property_name *api_key_id_property_name
220
+ self
221
+
222
+ end
223
+
224
+ # Sets the name used to query for the API Key Secret from a YAML instance. That is:
225
+ # <pre>
226
+ # apiKeyId = yml.access(<b>api_key_secret_property_name</b>)
227
+ # </pre>
228
+ #
229
+ # The Hash#access method searches through the provided path and returns the found value.
230
+ #
231
+ # The <b>api_key_secret_property_name</b> key can be as deep as needed, as long as it comes
232
+ # in the exact path order.
233
+ # Example: Having the file 'apiKey.yml' with the following content:
234
+ #
235
+ # stormpath:
236
+ # apiKey:
237
+ # secret: myStormpathApiKeyId
238
+ #
239
+ # The method should be called as follows:
240
+ #
241
+ # ClientApplicationBuilder#set_api_key_id_property_name('stormpath', 'apiKey', 'secret')
242
+ #
243
+ # @param api_key_secret_property_name the name used to query for the API Key Secret from a YAML instance.
244
+ # @return this ClientApplicationBuilder instance for method chaining.
245
+ def set_api_key_secret_property_name *api_key_secret_property_name
246
+
247
+ @client_builder.set_api_key_secret_property_name *api_key_secret_property_name
248
+ self
249
+
250
+ end
251
+
252
+ ##
253
+ # Sets the fully qualified Stormpath Application HREF (a URL) to use to acquire the Application instance when
254
+ # {@link #build_application} is called. See the Class-level RDoc for usage scenarios.
255
+ #
256
+ # @param applicationHref the fully qualified Stormpath Application HREF (a URL) to use to acquire the
257
+ # Application instance when {@link #build_application} is called.
258
+ # @return this ClientApplicationBuilder instance for method chaining.
259
+ def set_application_href application_href
260
+
261
+ @application_href = application_href
262
+ self
263
+
264
+ end
265
+
266
+ # Builds a Client and Application wrapper instance based on the configured
267
+ # {@link #set_application_href}. See the Class-level RDoc for usage scenarios.
268
+ #
269
+ # @return a Client and Application wrapper instance based on the configured {@link #set_application_href}.
270
+ def build
271
+
272
+ href = !@application_href.nil? ? @application_href.strip : nil
273
+
274
+ assert_false (href.nil? or href.empty?),
275
+ "'application_href' property must be specified when using this builder implementation."
276
+
277
+
278
+ cleaned_href = href
279
+
280
+ at_sigh_index = href.index '@'
281
+
282
+ if !at_sigh_index.nil?
283
+
284
+ parts = get_href_with_user_info href, at_sigh_index
285
+
286
+ cleaned_href = parts[0] + parts[2]
287
+
288
+ parts = parts[1].split ':', 2
289
+
290
+ api_key_properties = create_api_key_properties parts
291
+
292
+ set_api_key_properties api_key_properties
293
+
294
+ end #otherwise an apiKey File/YAML/etc for the API Key is required
295
+
296
+ client = build_client
297
+
298
+ application = client.data_store.get_resource cleaned_href, Application
299
+
300
+ ClientApplication.new client, application
301
+
302
+ end
303
+
304
+ protected
305
+
306
+ def build_client
307
+ @client_builder.build
308
+ end
309
+
310
+ def get_href_with_user_info href, at_sign_index
311
+
312
+ assert_kind_of String, href, 'href must be kind of String'
313
+ assert_kind_of Fixnum, at_sign_index, 'at_sign_index must be kind of Fixnum'
314
+
315
+ double_slash_index = href.index DOUBLE_SLASH
316
+
317
+ assert_not_nil double_slash_index, 'Invalid application href URL'
318
+
319
+ parts = Array.new 3
320
+
321
+ parts[0] = href[0..double_slash_index + 1] #up to and including the double slash
322
+ parts[1] = href[double_slash_index + DOUBLE_SLASH.length..at_sign_index -1] #raw user info
323
+ parts[2] = href[at_sign_index + 1..href.length - 1] #after the @ character
324
+
325
+ parts
326
+
327
+ end
328
+
329
+ def create_api_key_properties pair
330
+
331
+ assert_kind_of Array, pair, 'pair must be kind of Array'
332
+
333
+ assert_true (pair.length == 2), 'application_href userInfo segment must consist' +
334
+ ' of the following format: apiKeyId:apiKeySecret'
335
+
336
+ properties = Hash.new
337
+ properties.store 'apiKey.id', url_decode(pair[0])
338
+ properties.store 'apiKey.secret', url_decode(pair[1])
339
+
340
+ properties.to_yaml
341
+
342
+ end
343
+
344
+ def url_decode url
345
+ URI.decode url
346
+ end
347
+ end
348
+
349
+ end
350
+
351
+ end
@@ -28,7 +28,7 @@ module Stormpath
28
28
  # <pre>
29
29
  # location = "/home/jsmith/.stormpath/apiKey.yml";
30
30
  #
31
- # client = ClientBuilder.new.set_api_key_file_location(location).build()
31
+ # client = ClientBuilder.new.set_api_key_file_location(location).build
32
32
  # </pre>
33
33
  # <p/>
34
34
  # You may load files from the filesystem or URLs by specifying the file location.
@@ -102,14 +102,14 @@ module Stormpath
102
102
  # <pre>
103
103
  # location = "/home/jsmith/.stormpath/apiKey.yml";
104
104
  #
105
- # client = ClientBuilder.new.set_api_key_file_location(location).build()
105
+ # client = ClientBuilder.new.set_api_key_file_location(location).build
106
106
  # </pre>
107
107
  # <h3>Custom Property Names</h3>
108
108
  # If you want to control the property names used in the file, you may configure them via
109
109
  # {@link #set_api_key_id_property_name(String) set_api_key_id_property_name} and
110
110
  # {@link #set_api_key_secret_property_name(String) set_api_key_secret_property_name}.
111
111
  # <p/>
112
- # For example, if you had a {@code /home/jsmith/.stormpath/apiKey.properties} file with the following
112
+ # For example, if you had a {@code /home/jsmith/.stormpath/apiKey.yml} file with the following
113
113
  # name/value pairs:
114
114
  # <pre>
115
115
  # myStormpathApiKeyId = 'foo'
@@ -133,9 +133,11 @@ module Stormpath
133
133
  # @return the ClientBuilder instance for method chaining.
134
134
  #/
135
135
  def set_api_key_file_location location
136
+
136
137
  assert_kind_of String, location, 'location must be kind of String'
137
138
  @api_key_file_location = location
138
139
  self
140
+
139
141
  end
140
142
 
141
143
 
@@ -209,11 +211,9 @@ module Stormpath
209
211
 
210
212
  file = get_available_file
211
213
 
212
- if file.nil?
213
- raise ArgumentError, "No API Key file could be found or loaded from a file location. Please " +
214
- "configure the 'apiKeyFileLocation' property or alternatively configure a " +
215
- "YAML loadable instance."
216
- end
214
+ assert_not_nil file, "No API Key file could be found or loaded from a file location. Please " +
215
+ "configure the 'apiKeyFileLocation' property or alternatively configure a " +
216
+ "YAML loadable instance."
217
217
 
218
218
  yaml_obj = YAML::load(file)
219
219
 
@@ -237,8 +237,10 @@ module Stormpath
237
237
  end
238
238
 
239
239
  def set_base_url base_url
240
+
240
241
  @base_url = base_url
241
242
  self
243
+
242
244
  end
243
245
 
244
246
  private
@@ -277,13 +279,10 @@ module Stormpath
277
279
  prop_name_value,
278
280
  separator)
279
281
 
280
- if value.nil?
281
-
282
- raise ArgumentError, "There is no '" + prop_name.join(':') + "' property in the " +
283
- "configured apiKey YAML. You can either specify that property or " +
284
- "configure the " + masterName + "PropertyName value on the ClientBuilder to specify a " +
285
- "custom property name."
286
- end
282
+ assert_not_nil value, "There is no '" + prop_name.join(':') + "' property in the " +
283
+ "configured apiKey YAML. You can either specify that property or " +
284
+ "configure the #{masterName}_property_name value on the ClientBuilder to specify a " +
285
+ "custom property name."
287
286
 
288
287
  value
289
288
 
@@ -36,7 +36,7 @@ module Stormpath
36
36
  @resource_factory = ResourceFactory.new(self)
37
37
  end
38
38
 
39
- def instantiate(clazz, properties)
39
+ def instantiate(clazz, properties = {})
40
40
 
41
41
  @resource_factory.instantiate(clazz, properties)
42
42
  end
@@ -60,7 +60,7 @@ module Stormpath
60
60
 
61
61
  if resource.kind_of? return_type
62
62
 
63
- resource.set_properties returned_resource.properties
63
+ resource.set_properties to_hash(returned_resource)
64
64
 
65
65
  end
66
66
 
@@ -84,7 +84,7 @@ module Stormpath
84
84
  return_value = save_resource href, resource, clazz
85
85
 
86
86
  #ensure the caller's argument is updated with what is returned from the server:
87
- resource.set_properties return_value.properties
87
+ resource.set_properties to_hash(return_value)
88
88
 
89
89
  return_value
90
90
 
@@ -156,7 +156,7 @@ module Stormpath
156
156
  q_href = qualify q_href
157
157
  end
158
158
 
159
- response = execute_request('post', q_href, MultiJson.dump(resource.properties))
159
+ response = execute_request('post', q_href, MultiJson.dump(to_hash(resource)))
160
160
  @resource_factory.instantiate(return_type, response.to_hash)
161
161
 
162
162
  end
@@ -167,6 +167,41 @@ module Stormpath
167
167
  "https://" + DEFAULT_SERVER_HOST + "/v" + DEFAULT_API_VERSION.to_s
168
168
  end
169
169
 
170
+ def to_hash resource
171
+
172
+ property_names = resource.get_property_names
173
+ properties = Hash.new
174
+
175
+ property_names.each do |name|
176
+
177
+ property = resource.get_property name
178
+
179
+ if property.kind_of? Hash
180
+
181
+ property = to_simple_reference name, property
182
+
183
+ end
184
+
185
+ properties.store name, property
186
+
187
+ end
188
+
189
+ properties
190
+
191
+ end
192
+
193
+ def to_simple_reference property_name, hash
194
+
195
+ href_prop_name = Resource::Resource::HREF_PROP_NAME
196
+ assert_true (hash.kind_of? Hash and !hash.empty? and hash.has_key? href_prop_name), "Nested resource " +
197
+ "'#{property_name}' must have an 'href' property."
198
+
199
+ href = hash[href_prop_name]
200
+
201
+ {href_prop_name => href}
202
+
203
+ end
204
+
170
205
  end
171
206
 
172
207
  end
@@ -24,7 +24,7 @@ module Stormpath
24
24
  @data_store = data_store
25
25
  end
26
26
 
27
- def instantiate(clazz, constructor_args)
27
+ def instantiate(clazz, constructor_args = {})
28
28
 
29
29
  clazz.new @data_store, constructor_args
30
30
  end
@@ -118,6 +118,11 @@ module Stormpath
118
118
  get_resource_property GROUP_MEMBERSHIPS, GroupMembershipList
119
119
  end
120
120
 
121
+ protected
122
+ def printable_property? property_name
123
+ PASSWORD != property_name
124
+ end
125
+
121
126
  end
122
127
 
123
128
  end
@@ -23,12 +23,13 @@ module Stormpath
23
23
 
24
24
  HREF_PROP_NAME = "href"
25
25
 
26
- def initialize data_store, properties
26
+ def initialize data_store, properties = {}
27
27
 
28
28
  @data_store = data_store
29
29
  @read_lock = Mutex.new
30
30
  @write_lock = Mutex.new
31
31
  @properties = Hash.new
32
+ @dirty_properties = Hash.new
32
33
  set_properties properties
33
34
 
34
35
  end
@@ -39,10 +40,15 @@ module Stormpath
39
40
 
40
41
  begin
41
42
 
43
+ @properties.clear
44
+ @dirty_properties.clear
42
45
  @dirty = false
43
46
 
44
47
  if !properties.nil? and properties.is_a? Hash
45
48
  @properties.replace properties
49
+
50
+ # Don't consider this resource materialized if it is only a reference. A reference is any object that
51
+ # has only one 'href' property.
46
52
  href_only = @properties.size == 1 and @properties.has_key? HREF_PROP_NAME
47
53
  @materialized = !href_only
48
54
 
@@ -61,8 +67,28 @@ module Stormpath
61
67
  #not the href/id, must be a property that requires materialization:
62
68
  if !is_new and !materialized
63
69
 
64
- materialize
70
+ # only materialize if the property hasn't been set previously (no need to execute a server
71
+ # request since we have the most recent value already):
72
+ present = false
73
+ @read_lock.lock
74
+
75
+ begin
76
+
77
+ present = @dirty_properties.has_key? name
78
+
79
+ ensure
80
+
81
+ @read_lock.unlock
82
+
83
+ end
84
+
85
+ if !present
86
+ # exhausted present properties - we require a server call:
87
+ materialize
88
+ end
89
+
65
90
  end
91
+
66
92
  end
67
93
 
68
94
  read_property name
@@ -83,7 +109,105 @@ module Stormpath
83
109
  get_property HREF_PROP_NAME
84
110
  end
85
111
 
86
- attr_reader :properties
112
+ def inspect
113
+
114
+ @read_lock.lock
115
+
116
+ str = ''
117
+
118
+ begin
119
+
120
+ counter = 2
121
+ @properties.each do |key, value|
122
+
123
+ if str.empty?
124
+
125
+ str = '#<' + class_name_with_id + ' @properties={'
126
+
127
+ else
128
+
129
+ if printable_property? key
130
+
131
+ str << "\"#{key}\"=>"
132
+
133
+ if value.kind_of? Hash and value.has_key? HREF_PROP_NAME
134
+
135
+ str << '{"' << HREF_PROP_NAME + '"=>"' + value[HREF_PROP_NAME] + '"}'
136
+
137
+ else
138
+
139
+ str << "\"#{value}\""
140
+
141
+ end
142
+
143
+ if counter < @properties.length
144
+
145
+ str << ', '
146
+
147
+ end
148
+
149
+ end
150
+
151
+ counter+= 1
152
+
153
+ end
154
+
155
+ end
156
+
157
+ ensure
158
+
159
+ @read_lock.unlock
160
+
161
+ end
162
+
163
+ if !str.empty?
164
+ str << '}>'
165
+ end
166
+
167
+ str
168
+
169
+ end
170
+
171
+ def to_s
172
+ '#<' + class_name_with_id + '>'
173
+ end
174
+
175
+ def to_yaml
176
+
177
+ yaml = '--- !ruby/object:' << self.class.name << "\n"
178
+
179
+ @read_lock.lock
180
+
181
+ begin
182
+
183
+ first_property = true
184
+ @properties.each do |key, value|
185
+
186
+ if printable_property? key
187
+
188
+ if first_property
189
+
190
+ yaml << " properties\n "
191
+
192
+ end
193
+
194
+ yaml << ' ' << key << ': ' << value << "\n"
195
+
196
+ first_property = false
197
+
198
+ end
199
+
200
+ end
201
+
202
+ ensure
203
+
204
+ @read_lock.unlock
205
+
206
+ end
207
+
208
+ yaml << "\n"
209
+
210
+ end
87
211
 
88
212
  protected
89
213
 
@@ -126,18 +250,9 @@ module Stormpath
126
250
  @write_lock.lock
127
251
 
128
252
  begin
129
- if value.nil?
130
-
131
- removed = @properties.delete name
132
-
133
- if !removed.nil?
134
- @dirty = true
135
- end
136
-
137
- else
138
- @properties.store name, value
139
- @dirty = true
140
- end
253
+ @properties.store name, value
254
+ @dirty_properties.store name, value
255
+ @dirty = true
141
256
  ensure
142
257
  @write_lock.unlock
143
258
  end
@@ -153,12 +268,32 @@ module Stormpath
153
268
 
154
269
  resource = @data_store.get_resource get_href, clazz
155
270
  @properties.replace resource.properties
271
+
272
+ #retain dirty properties:
273
+ @properties.merge! @dirty_properties
274
+
156
275
  @materialized = true
157
276
 
158
277
  ensure
159
278
 
160
279
  @write_lock.unlock
161
280
  end
281
+
282
+ end
283
+
284
+ def properties
285
+ @properties
286
+ end
287
+
288
+ ##
289
+ # Returns {@code true} if the internal property is safe to print in to_s or inspect, {@code false} otherwise.
290
+ #
291
+ # @param property_name The name of the property to check for safe printing
292
+ # @return {@code true} if the internal property is safe to print in to_s, {@code false} otherwise.
293
+ def printable_property? property_name
294
+
295
+ return true
296
+
162
297
  end
163
298
 
164
299
  private
@@ -182,6 +317,10 @@ module Stormpath
182
317
  end
183
318
 
184
319
  end
320
+
321
+ def class_name_with_id
322
+ self.class.name + ':0x' + ('%x' % (self.object_id << 1)).to_s
323
+ end
185
324
  end
186
325
  end
187
326
 
@@ -36,6 +36,13 @@ module Stormpath
36
36
  raise ArgumentError, message, caller unless arg
37
37
 
38
38
  end
39
+
40
+ def assert_false arg, message
41
+
42
+ raise ArgumentError, message, caller unless !arg
43
+
44
+ end
45
+
39
46
  end
40
47
  end
41
48
  end
@@ -14,6 +14,6 @@
14
14
  # limitations under the License.
15
15
  #
16
16
  module Stormpath
17
- VERSION = '0.2.0'
18
- VERSION_DATE = '2012-08-20'
17
+ VERSION = '0.3.0'
18
+ VERSION_DATE = '2012-08-31'
19
19
  end
@@ -0,0 +1,114 @@
1
+ require "stormpath-sdk"
2
+
3
+ include Stormpath::Client
4
+
5
+ describe "Client Application Builder Tests" do
6
+
7
+ before(:all) do
8
+ @client_file = 'test/client/client.yml'
9
+ @client_remote_file = 'http://localhost:8081/client.yml'
10
+ @application_href = 'http://localhost:8080/v1/applications/A0atUpZARYGApaN5f88O3A'
11
+ @http_prefix = 'http://'
12
+ @app_href_without_http = '@localhost:8080/v1/applications/A0atUpZARYGApaN5f88O3A'
13
+ @client_builder = ClientBuilder.new.set_base_url 'http://localhost:8080/v1'
14
+ @test_remote_file = false
15
+ end
16
+
17
+
18
+ it 'Builder should read default properties from YAML file location with application href' do
19
+
20
+ result = ClientApplicationBuilder.new(@client_builder).
21
+ set_api_key_file_location(@client_file).
22
+ set_application_href(@application_href).
23
+ build
24
+
25
+ result.should be_kind_of ClientApplication
26
+
27
+ end
28
+
29
+ it 'Builder should create ClientApplication with data from application href with credentials' do
30
+
31
+ # getting the properties from file...just to avoid writing them directly
32
+ # in the 'properties' Hash
33
+ yml_obj = YAML::load(File.open @client_file)
34
+ api_key_id_keyword = 'apiKey.id'
35
+ api_key_secret_keyword = 'apiKey.secret'
36
+
37
+ # we create the client from this Hash instead of from a file
38
+ properties = {api_key_id_keyword => yml_obj[api_key_id_keyword],
39
+ api_key_secret_keyword => yml_obj[api_key_secret_keyword]}
40
+
41
+ application_href = @http_prefix +
42
+ properties[api_key_id_keyword] +
43
+ ':' +
44
+ properties[api_key_secret_keyword] +
45
+ @app_href_without_http
46
+
47
+ result = ClientApplicationBuilder.new(@client_builder).
48
+ set_application_href(application_href).
49
+ build
50
+
51
+ result.should be_kind_of ClientApplication
52
+
53
+ end
54
+
55
+ it 'Builder should read custom complex properties from YAML file locatio with application href' do
56
+
57
+ @client_builder = ClientBuilder.new.set_base_url 'http://localhost:8080/v1'
58
+ result = ClientApplicationBuilder.new(@client_builder).
59
+ set_api_key_file_location(@client_file).
60
+ set_api_key_id_property_name('stormpath', 'apiKey', 'id').
61
+ set_api_key_secret_property_name('stormpath', 'apiKey', 'secret').
62
+ set_application_href(@application_href).
63
+ build
64
+
65
+ result.should be_kind_of ClientApplication
66
+
67
+ end
68
+
69
+ it 'Builder should read custom simple properties from YAML file locatio with application href' do
70
+
71
+ # getting the properties from file...just to avoid writing them directly
72
+ # in the 'properties' Hash
73
+ yml_obj = YAML::load(File.open @client_file)
74
+ api_key_id_keyword = 'different.apiKey.id'
75
+ api_key_secret_keyword = 'different.apiKey.secret'
76
+
77
+ # we create the client from this Hash instead of from a file
78
+ properties = {api_key_id_keyword => yml_obj[api_key_id_keyword],
79
+ api_key_secret_keyword => yml_obj[api_key_secret_keyword]}
80
+
81
+ result = ClientApplicationBuilder.new(@client_builder).
82
+ set_api_key_properties(properties.to_yaml).
83
+ set_api_key_id_property_name(api_key_id_keyword).
84
+ set_api_key_secret_property_name(api_key_secret_keyword).
85
+ set_application_href(@application_href).
86
+ build
87
+
88
+ result.should be_kind_of ClientApplication
89
+
90
+ end
91
+
92
+ it 'Builder should throw exception when creating it with wrong argument' do
93
+
94
+ expect { ClientApplicationBuilder.new 'WRONG' }.to raise_error ArgumentError
95
+
96
+ end
97
+
98
+ it 'Builder should throw exception when trying to build without application href' do
99
+
100
+ expect { ClientApplicationBuilder.new(@client_builder).build }.to raise_error ArgumentError
101
+
102
+ end
103
+
104
+ it 'Builder should throw exception when trying to build with an invalid application href' do
105
+
106
+ expect { ClientApplicationBuilder.new(@client_builder).
107
+ set_api_key_file_location(@client_file).
108
+ set_application_href('id:secret@stormpath.com/v1').
109
+ build }.
110
+ to raise_error ArgumentError
111
+
112
+ end
113
+
114
+ end
@@ -237,6 +237,19 @@ describe "READ Operations" do
237
237
 
238
238
  end
239
239
 
240
+ it "dirty properties must be retained after materialization" do
241
+
242
+ account = @data_store.instantiate Account, {'href' => 'accounts/gJH4bh6QQKK0awRmwD72Cg'}
243
+
244
+ name = 'Name Before Materialization'
245
+
246
+ account.set_given_name name
247
+
248
+ account.get_surname.should be_kind_of String
249
+
250
+ account.get_given_name.should == name
251
+ end
252
+
240
253
  end
241
254
 
242
255
 
@@ -10,10 +10,12 @@ describe "WRITE Operations" do
10
10
  @data_store = @client.data_store
11
11
  @create_account = false
12
12
  @update_account = false
13
+ @remove_account_property = false
13
14
  @update_application = false
14
15
  @update_directory = false
15
16
  @update_group = false
16
17
  @create_application = false
18
+ @change_password = false
17
19
  @verify_email = false
18
20
  @send_password_reset_email = false
19
21
  @verify_password_reset_token = false
@@ -25,10 +27,10 @@ describe "WRITE Operations" do
25
27
 
26
28
  it "application should be able to authenticate" do
27
29
 
28
- href = 'applications/fzyWJ5V_SDORGPk4fT2jhA'
30
+ href = 'applications/A0atUpZARYGApaN5f88O3A'
29
31
  application = @data_store.get_resource href, Application
30
32
 
31
- result = application.authenticate_account UsernamePasswordRequest.new 'tootertest', 'super_P4ss', nil
33
+ result = application.authenticate_account UsernamePasswordRequest.new 'kentucky', 'super_P4ss'
32
34
 
33
35
  result.should be_kind_of AuthenticationResult
34
36
 
@@ -39,9 +41,9 @@ describe "WRITE Operations" do
39
41
 
40
42
  begin
41
43
 
42
- href = 'applications/fzyWJ5V_SDORGPk4fT2jhA'
44
+ href = 'applications/A0atUpZARYGApaN5f88O3A'
43
45
  application = @data_store.get_resource href, Application
44
- result = application.authenticate_account UsernamePasswordRequest.new 'tootertest', 'WRONG_PASS', nil
46
+ result = application.authenticate_account UsernamePasswordRequest.new 'kentucky', 'WRONG_PASS'
45
47
 
46
48
  rescue ResourceError => re
47
49
  p '** Authentication Error **'
@@ -59,17 +61,17 @@ describe "WRITE Operations" do
59
61
 
60
62
  if (@create_account)
61
63
 
62
- href = 'directories/wDTY5jppTLS2uZEAcqaL5A'
64
+ href = 'directories/_oIg8zU5QWyiz22DcVYVLg'
63
65
  directory = @data_store.get_resource href, Directory
64
66
 
65
- account = @data_store.instantiate Account, nil
67
+ account = @data_store.instantiate Account
66
68
  account.set_email 'rubysdk@email.com'
67
69
  account.set_given_name 'Ruby'
68
70
  account.set_password 'super_P4ss'
69
71
  account.set_surname 'Sdk'
70
72
  account.set_username 'rubysdk'
71
73
 
72
- result = directory.create_account account
74
+ result = directory.create_account account, false
73
75
 
74
76
  result.should be_kind_of Account
75
77
 
@@ -96,6 +98,30 @@ describe "WRITE Operations" do
96
98
 
97
99
  end
98
100
 
101
+ it "account property should be updated/removed" do
102
+
103
+ if (@remove_account_property)
104
+
105
+ href = 'accounts/gJH4bh6QQKK0awRmwD72Cg'
106
+ account = @data_store.get_resource href, Account
107
+
108
+ mod_value = 'Modified at: ' + Time.now.to_s
109
+ account.set_middle_name mod_value
110
+
111
+ account.save
112
+
113
+ account.get_middle_name.should == mod_value
114
+
115
+ account.set_middle_name nil
116
+
117
+ account.save
118
+
119
+ account.get_middle_name.should == nil
120
+
121
+ end
122
+
123
+ end
124
+
99
125
  it "application should be updated" do
100
126
 
101
127
  if (@update_application)
@@ -156,7 +182,7 @@ describe "WRITE Operations" do
156
182
 
157
183
  tenant = @client.current_tenant
158
184
 
159
- application = @data_store.instantiate Application, nil
185
+ application = @data_store.instantiate Application
160
186
  application.set_name 'Test Application Creation'
161
187
  application.set_description 'Test Application Description'
162
188
 
@@ -214,6 +240,35 @@ describe "WRITE Operations" do
214
240
 
215
241
  end
216
242
 
243
+ it "password should be changed" do
244
+
245
+ if (@change_password)
246
+
247
+ href = 'applications/fzyWJ5V_SDORGPk4fT2jhA'
248
+ application = @data_store.get_resource href, Application
249
+
250
+ account = application.verify_password_reset_token 'TFMWt3lbQdWc7MNF48pJbw'
251
+
252
+ account.should be_kind_of Account
253
+
254
+ new_password = 'changed_P4ss'
255
+ account.set_password new_password
256
+ account.save
257
+
258
+ begin
259
+
260
+ application.authenticate_account UsernamePasswordRequest.new account.get_username, new_password
261
+
262
+ rescue ResourceError => re
263
+
264
+ false.should be true
265
+
266
+ end
267
+
268
+ end
269
+
270
+ end
271
+
217
272
  it "account should be created linked to a group" do
218
273
 
219
274
  if (@create_account_with_group_membership)
@@ -224,7 +279,7 @@ describe "WRITE Operations" do
224
279
  group_href = 'groups/mCidbrAcSF-VpkNfOVvJkQ'
225
280
  group = @data_store.get_resource group_href, Group
226
281
 
227
- account = @data_store.instantiate Account, nil
282
+ account = @data_store.instantiate Account
228
283
  account.set_email 'rubysdkwithgroup@email.com'
229
284
  account.set_given_name 'Ruby'
230
285
  account.set_password 'super_P4ss'
@@ -0,0 +1,41 @@
1
+ require "test/resource/test_resource"
2
+
3
+ describe "Resource Tests" do
4
+
5
+ it "non materialized resource get dirty property without materializing" do
6
+
7
+ props = {'href' => 'http://foo.com/test/123'}
8
+ data_store = Stormpath::DataStore::DataStore.new '', ''
9
+
10
+ test_resource = TestResource.new data_store, props
11
+ name = 'New Name'
12
+ test_resource.set_name name
13
+
14
+ begin
15
+
16
+ name.should == test_resource.get_name
17
+
18
+ rescue Exception => e
19
+
20
+ true.should be false
21
+
22
+ end
23
+
24
+ end
25
+
26
+ it "password property must not show up on inspect" do
27
+
28
+ props = {'href' => 'http://foo.com/test/123'}
29
+ data_store = Stormpath::DataStore::DataStore.new '', ''
30
+
31
+ test_resource = TestResource.new data_store, props
32
+ name = 'New Name'
33
+ test_resource.set_name name
34
+
35
+ test_resource.set_password 'my_password'
36
+
37
+ test_resource.inspect.should_not include 'password'
38
+
39
+ end
40
+
41
+ end
@@ -0,0 +1,28 @@
1
+ class TestResource < Stormpath::Resource::Resource
2
+
3
+ def get_name
4
+ get_property 'name'
5
+ end
6
+
7
+ def set_name name
8
+ set_property 'name', name
9
+ end
10
+
11
+ def get_description
12
+ get_property 'description'
13
+ end
14
+
15
+ def set_description description
16
+ set_property 'description', description
17
+ end
18
+
19
+ def set_password password
20
+ set_property 'password', password
21
+ end
22
+
23
+ protected
24
+ def printable_property? property_name
25
+ 'password' != property_name
26
+ end
27
+
28
+ end
metadata CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 0
7
- - 2
7
+ - 3
8
8
  - 0
9
- version: 0.2.0
9
+ version: 0.3.0
10
10
  platform: ruby
11
11
  authors:
12
12
  - Elder Crisostomo
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2012-08-20 00:00:00 -07:00
17
+ date: 2012-08-31 00:00:00 -07:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
@@ -138,6 +138,8 @@ extensions: []
138
138
  extra_rdoc_files: []
139
139
 
140
140
  files:
141
+ - .gitignore
142
+ - CHANGES.md
141
143
  - Gemfile
142
144
  - README.md
143
145
  - Rakefile
@@ -148,6 +150,8 @@ files:
148
150
  - lib/stormpath-sdk/auth/username_password_request.rb
149
151
  - lib/stormpath-sdk/client/api_key.rb
150
152
  - lib/stormpath-sdk/client/client.rb
153
+ - lib/stormpath-sdk/client/client_application.rb
154
+ - lib/stormpath-sdk/client/client_application_builder.rb
151
155
  - lib/stormpath-sdk/client/client_builder.rb
152
156
  - lib/stormpath-sdk/ds/data_store.rb
153
157
  - lib/stormpath-sdk/ds/resource_factory.rb
@@ -181,9 +185,12 @@ files:
181
185
  - lib/stormpath-sdk/version.rb
182
186
  - stormpath-sdk.gemspec
183
187
  - test/client/client.yml
184
- - test/client/clientbuilder_spec.rb
188
+ - test/client/client_application_builder_spec.rb
189
+ - test/client/client_builder_spec.rb
185
190
  - test/client/read_spec.rb
186
191
  - test/client/write_spec.rb
192
+ - test/resource/resource_spec.rb
193
+ - test/resource/test_resource.rb
187
194
  has_rdoc: true
188
195
  homepage: https://github.com/stormpath/stormpath-sdk-ruby
189
196
  licenses: []
@@ -219,6 +226,9 @@ signing_key:
219
226
  specification_version: 3
220
227
  summary: Stormpath SDK
221
228
  test_files:
222
- - test/client/clientbuilder_spec.rb
229
+ - test/client/client_application_builder_spec.rb
230
+ - test/client/client_builder_spec.rb
223
231
  - test/client/read_spec.rb
224
232
  - test/client/write_spec.rb
233
+ - test/resource/resource_spec.rb
234
+ - test/resource/test_resource.rb