stormpath-sdk 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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