stormpath-sdk 1.0.0.beta.4 → 1.0.0.beta.5
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/CHANGES.md +15 -0
- data/README.md +35 -3
- data/lib/stormpath-sdk.rb +5 -6
- data/lib/stormpath-sdk/auth/basic_authenticator.rb +2 -0
- data/lib/stormpath-sdk/auth/basic_login_attempt.rb +11 -0
- data/lib/stormpath-sdk/auth/username_password_request.rb +6 -12
- data/lib/stormpath-sdk/data_store.rb +118 -127
- data/lib/stormpath-sdk/http/http_client_request_executor.rb +10 -42
- data/lib/stormpath-sdk/resource/account.rb +13 -3
- data/lib/stormpath-sdk/resource/account_membership.rb +16 -0
- data/lib/stormpath-sdk/resource/account_status.rb +26 -0
- data/lib/stormpath-sdk/resource/account_store_mapping.rb +4 -2
- data/lib/stormpath-sdk/resource/application.rb +4 -2
- data/lib/stormpath-sdk/resource/associations.rb +7 -3
- data/lib/stormpath-sdk/resource/base.rb +21 -15
- data/lib/stormpath-sdk/resource/custom_data.rb +86 -0
- data/lib/stormpath-sdk/resource/custom_data_hash_methods.rb +33 -0
- data/lib/stormpath-sdk/resource/custom_data_storage.rb +39 -0
- data/lib/stormpath-sdk/resource/directory.rb +4 -4
- data/lib/stormpath-sdk/resource/expansion.rb +15 -0
- data/lib/stormpath-sdk/resource/group.rb +10 -0
- data/lib/stormpath-sdk/resource/status.rb +16 -5
- data/lib/stormpath-sdk/version.rb +2 -2
- data/spec/client_spec.rb +6 -1
- data/spec/data_store_spec.rb +7 -2
- data/spec/resource/account_spec.rb +73 -30
- data/spec/resource/account_store_mapping_spec.rb +20 -5
- data/spec/resource/application_spec.rb +135 -0
- data/spec/resource/custom_data_spec.rb +198 -0
- data/spec/resource/directory_spec.rb +192 -9
- data/spec/resource/group_membership_spec.rb +35 -0
- data/spec/resource/group_spec.rb +44 -26
- data/spec/resource/status_spec.rb +81 -0
- data/spec/resource/tenant_spec.rb +19 -0
- data/stormpath-sdk.gemspec +2 -2
- metadata +13 -3
data/.gitignore
CHANGED
data/CHANGES.md
CHANGED
@@ -1,6 +1,21 @@
|
|
1
1
|
stormpath-sdk-ruby Changelog
|
2
2
|
============================
|
3
3
|
|
4
|
+
Version 1.0.0.beta.5
|
5
|
+
--------------------
|
6
|
+
|
7
|
+
Released on March 3, 2014
|
8
|
+
|
9
|
+
- Added the Custom Data resource
|
10
|
+
- Added CustomDataStorage module
|
11
|
+
- Specify an AccountStore during authentication
|
12
|
+
- Added AccountStatus module (specialization of the Status module)
|
13
|
+
- Added Status spec
|
14
|
+
- Added AccountMemberships
|
15
|
+
- Added GroupMemberships spec
|
16
|
+
- Send only dirty properties to the API
|
17
|
+
- Fixed REDIRECTS_LIMIT issue
|
18
|
+
|
4
19
|
Version 1.0.0.beta.4
|
5
20
|
--------------------
|
6
21
|
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
[![Build Status](https://api.travis-ci.org/stormpath/stormpath-sdk-ruby.png?branch=master)](https://travis-ci.org/stormpath/stormpath-sdk-ruby)
|
1
|
+
[![Build Status](https://api.travis-ci.org/stormpath/stormpath-sdk-ruby.png?branch=master,development)](https://travis-ci.org/stormpath/stormpath-sdk-ruby)
|
2
2
|
|
3
3
|
# Stormpath Ruby SDK
|
4
4
|
|
@@ -416,7 +416,7 @@ Group membership can be created by:
|
|
416
416
|
* Explicitly creating a group membership resource with your client:
|
417
417
|
|
418
418
|
```ruby
|
419
|
-
|
419
|
+
group_membership = client.group_memberships.create group: group, account: account
|
420
420
|
```
|
421
421
|
|
422
422
|
* Using the <code>add_group</code> method on the account instance:
|
@@ -428,11 +428,43 @@ Group membership can be created by:
|
|
428
428
|
* Using the <code>add_account</code> method on the group instance:
|
429
429
|
|
430
430
|
```ruby
|
431
|
-
group.
|
431
|
+
group.add_account account
|
432
432
|
```
|
433
433
|
|
434
434
|
You will need to reload the account or group resource after these
|
435
435
|
operations to ensure they've picked up the changes.
|
436
|
+
### Add Custom Data to Accounts or Groups
|
437
|
+
|
438
|
+
Account and Group resources have predefined fields that are useful to many applications, but you are likely to have your own custom data that you need to associate with an account or group as well.
|
439
|
+
|
440
|
+
For this reason, both the account and group resources support a linked custom_data resource that you can use for your own needs.
|
441
|
+
|
442
|
+
*Set Custom Data*
|
443
|
+
```ruby
|
444
|
+
account = Stormpath::Resource::Account.new({ email: "test@example.com", given_name: 'Ruby SDK', password: 'P@$$w0rd', surname: 'SDK',})
|
445
|
+
|
446
|
+
account.custom_data["rank"] = "Captain"
|
447
|
+
account.custom_data["birth_date"] = "2305-07-13"
|
448
|
+
account.custom_data["birth_place"] = "La Barre, France"
|
449
|
+
|
450
|
+
directory.create_account account
|
451
|
+
```
|
452
|
+
|
453
|
+
Notice how we did not call account.custom_data.save - creating the account (or updating it later via save) will automatically persist the account's customData resource. The account 'knows' that the custom data resource has been changed and it will propogate those changes automatically when you persist the account.
|
454
|
+
|
455
|
+
Groups work the same way - you can save a group and it's custom data resource will be saved as well.
|
456
|
+
|
457
|
+
*Delete a specific Custom Data field*
|
458
|
+
```ruby
|
459
|
+
account.custom_data["birth_date"] #=> "2305-07-13"
|
460
|
+
account.custom_data.delete("birth_date")
|
461
|
+
account.custom_data.save
|
462
|
+
```
|
463
|
+
|
464
|
+
*Delete all Custom Data*
|
465
|
+
```ruby
|
466
|
+
account.custom_data.delete
|
467
|
+
```
|
436
468
|
|
437
469
|
## Testing
|
438
470
|
|
data/lib/stormpath-sdk.rb
CHANGED
@@ -25,28 +25,27 @@ module Stormpath
|
|
25
25
|
module Resource
|
26
26
|
autoload :Expansion, 'stormpath-sdk/resource/expansion'
|
27
27
|
autoload :Status, 'stormpath-sdk/resource/status'
|
28
|
+
autoload :AccountStatus, 'stormpath-sdk/resource/account_status'
|
28
29
|
autoload :Utils, 'stormpath-sdk/resource/utils'
|
29
30
|
autoload :Associations, 'stormpath-sdk/resource/associations'
|
30
31
|
autoload :Base, 'stormpath-sdk/resource/base'
|
31
32
|
autoload :Error, 'stormpath-sdk/resource/error'
|
32
33
|
autoload :Instance, 'stormpath-sdk/resource/instance'
|
33
34
|
autoload :Collection, 'stormpath-sdk/resource/collection'
|
35
|
+
autoload :CustomData, 'stormpath-sdk/resource/custom_data'
|
36
|
+
autoload :CustomDataStorage, 'stormpath-sdk/resource/custom_data_storage'
|
37
|
+
autoload :CustomDataHashMethods, 'stormpath-sdk/resource/custom_data_hash_methods'
|
34
38
|
autoload :Tenant, 'stormpath-sdk/resource/tenant'
|
35
39
|
autoload :Application, 'stormpath-sdk/resource/application'
|
36
|
-
autoload :Applications, 'stormpath-sdk/resource/applications'
|
37
40
|
autoload :Directory, 'stormpath-sdk/resource/directory'
|
38
|
-
autoload :Directories, 'stormpath-sdk/resource/directories'
|
39
41
|
autoload :Account, 'stormpath-sdk/resource/account'
|
40
|
-
autoload :Accounts, 'stormpath-sdk/resource/accounts'
|
41
42
|
autoload :AccountStore, 'stormpath-sdk/resource/account_store'
|
42
43
|
autoload :AccountStoreMapping, 'stormpath-sdk/resource/account_store_mapping'
|
43
44
|
autoload :Group, 'stormpath-sdk/resource/group'
|
44
|
-
autoload :Groups, 'stormpath-sdk/resource/groups'
|
45
45
|
autoload :EmailVerificationToken, 'stormpath-sdk/resource/email_verification_token'
|
46
46
|
autoload :GroupMembership, 'stormpath-sdk/resource/group_membership'
|
47
|
-
autoload :
|
47
|
+
autoload :AccountMembership, 'stormpath-sdk/resource/account_membership'
|
48
48
|
autoload :PasswordResetToken, 'stormpath-sdk/resource/password_reset_token'
|
49
|
-
autoload :PasswordResetTokens, 'stormpath-sdk/resource/password_reset_tokens'
|
50
49
|
end
|
51
50
|
|
52
51
|
module Cache
|
@@ -19,6 +19,17 @@ module Stormpath
|
|
19
19
|
|
20
20
|
TYPE = "type"
|
21
21
|
VALUE = "value"
|
22
|
+
ACCOUNT_STORE = "account_store"
|
23
|
+
|
24
|
+
def account_store
|
25
|
+
get_property ACCOUNT_STORE
|
26
|
+
end
|
27
|
+
|
28
|
+
def account_store=(account_store)
|
29
|
+
if account_store.kind_of? Stormpath::Resource::Base
|
30
|
+
set_property ACCOUNT_STORE, {HREF_PROP_NAME => account_store.href}
|
31
|
+
end
|
32
|
+
end
|
22
33
|
|
23
34
|
def type
|
24
35
|
get_property TYPE
|
@@ -14,17 +14,16 @@
|
|
14
14
|
# limitations under the License.
|
15
15
|
#
|
16
16
|
module Stormpath
|
17
|
-
|
18
17
|
module Authentication
|
19
|
-
|
20
18
|
class UsernamePasswordRequest
|
21
19
|
|
22
|
-
attr_reader :host
|
20
|
+
attr_reader :host, :account_store
|
23
21
|
|
24
|
-
def initialize username, password,
|
22
|
+
def initialize username, password, options = {}
|
25
23
|
@username = username
|
26
24
|
@password = (password != nil and password.length > 0) ? password.chars.to_a : "".chars.to_a
|
27
|
-
@host = host
|
25
|
+
@host = options[:host]
|
26
|
+
@account_store = options[:account_store]
|
28
27
|
end
|
29
28
|
|
30
29
|
def principals
|
@@ -38,18 +37,13 @@ module Stormpath
|
|
38
37
|
def clear
|
39
38
|
@username = nil
|
40
39
|
@host = nil
|
40
|
+
@account_store = nil
|
41
41
|
|
42
|
-
@password.each { |pass_char|
|
43
|
-
|
44
|
-
pass_char = 0x00
|
45
|
-
}
|
46
|
-
|
42
|
+
@password.each { |pass_char| pass_char = 0x00 }
|
47
43
|
@password = nil
|
48
44
|
end
|
49
45
|
|
50
46
|
end
|
51
|
-
|
52
47
|
end
|
53
|
-
|
54
48
|
end
|
55
49
|
|
@@ -19,16 +19,17 @@ class Stormpath::DataStore
|
|
19
19
|
|
20
20
|
DEFAULT_SERVER_HOST = "api.stormpath.com"
|
21
21
|
DEFAULT_API_VERSION = 1
|
22
|
+
HREF_PROP_NAME = Stormpath::Resource::Base::HREF_PROP_NAME
|
22
23
|
|
23
|
-
CACHE_REGIONS = %w( applications directories accounts groups groupMemberships tenants )
|
24
|
+
CACHE_REGIONS = %w( applications directories accounts groups groupMemberships accountMemberships tenants customData )
|
24
25
|
|
25
|
-
attr_reader :client, :request_executor
|
26
|
+
attr_reader :client, :request_executor, :cache_manager
|
26
27
|
|
27
|
-
def initialize(request_executor, cache_opts, client,
|
28
|
+
def initialize(request_executor, cache_opts, client, base_url = nil)
|
28
29
|
assert_not_nil request_executor, "RequestExecutor cannot be null."
|
29
30
|
|
30
31
|
@client = client
|
31
|
-
@base_url = get_base_url(
|
32
|
+
@base_url = get_base_url(base_url)
|
32
33
|
@request_executor = request_executor
|
33
34
|
initialize_cache cache_opts
|
34
35
|
end
|
@@ -48,11 +49,7 @@ class Stormpath::DataStore
|
|
48
49
|
end
|
49
50
|
|
50
51
|
def get_resource(href, clazz, query=nil)
|
51
|
-
q_href =
|
52
|
-
qualify href
|
53
|
-
else
|
54
|
-
href
|
55
|
-
end
|
52
|
+
q_href = qualify href
|
56
53
|
|
57
54
|
data = execute_request('get', q_href, nil, query)
|
58
55
|
instantiate clazz, data.to_hash
|
@@ -63,7 +60,7 @@ class Stormpath::DataStore
|
|
63
60
|
parent_href = "#{parent_href}?#{URI.encode_www_form(options)}" unless options.empty?
|
64
61
|
save_resource(parent_href, resource, return_type).tap do |returned_resource|
|
65
62
|
if resource.kind_of? return_type
|
66
|
-
resource.set_properties
|
63
|
+
resource.set_properties returned_resource.properties
|
67
64
|
end
|
68
65
|
end
|
69
66
|
end
|
@@ -73,13 +70,10 @@ class Stormpath::DataStore
|
|
73
70
|
assert_kind_of Stormpath::Resource::Base, resource, "resource argument must be instance of Stormpath::Resource::Base"
|
74
71
|
|
75
72
|
href = resource.href
|
73
|
+
assert_not_nil href, "href or resource.href cannot be null."
|
76
74
|
assert_true href.length > 0, "save may only be called on objects that have already been persisted (i.e. they have an existing href)."
|
77
75
|
|
78
|
-
href =
|
79
|
-
qualify(href)
|
80
|
-
else
|
81
|
-
href
|
82
|
-
end
|
76
|
+
href = qualify(href)
|
83
77
|
|
84
78
|
clazz ||= resource.class
|
85
79
|
|
@@ -88,156 +82,153 @@ class Stormpath::DataStore
|
|
88
82
|
end
|
89
83
|
end
|
90
84
|
|
91
|
-
def delete(resource)
|
85
|
+
def delete(resource, property_name = nil)
|
92
86
|
assert_not_nil resource, "resource argument cannot be null."
|
93
87
|
assert_kind_of Stormpath::Resource::Base, resource, "resource argument must be instance of Stormpath::Resource::Base"
|
94
88
|
|
95
|
-
|
89
|
+
href = resource.href
|
90
|
+
href += "/#{property_name}" if property_name
|
91
|
+
href = qualify(href)
|
92
|
+
|
93
|
+
execute_request('delete', href)
|
96
94
|
end
|
97
95
|
|
98
|
-
|
99
|
-
@cache_manager
|
100
|
-
end
|
96
|
+
private
|
101
97
|
|
102
|
-
|
98
|
+
def needs_to_be_fully_qualified(href)
|
99
|
+
!href.downcase.start_with? 'http'
|
100
|
+
end
|
103
101
|
|
104
|
-
|
105
|
-
|
106
|
-
|
102
|
+
def qualify(href)
|
103
|
+
if needs_to_be_fully_qualified(href)
|
104
|
+
slash_added = href.start_with?('/') ? '' : '/'
|
105
|
+
@base_url + slash_added + href
|
106
|
+
else
|
107
|
+
href
|
108
|
+
end
|
109
|
+
end
|
107
110
|
|
108
|
-
|
109
|
-
|
111
|
+
def execute_request(http_method, href, body=nil, query=nil)
|
112
|
+
if http_method == 'get' && (cache = cache_for href)
|
113
|
+
cached_result = cache.get href
|
114
|
+
return cached_result if cached_result
|
115
|
+
end
|
110
116
|
|
111
|
-
|
112
|
-
|
113
|
-
|
117
|
+
request = Request.new(http_method, href, query, Hash.new, body)
|
118
|
+
apply_default_request_headers request
|
119
|
+
response = @request_executor.execute_request request
|
120
|
+
result = response.body.length > 0 ? MultiJson.load(response.body) : ''
|
114
121
|
|
115
|
-
|
116
|
-
|
122
|
+
if response.error?
|
123
|
+
error = Stormpath::Resource::Error.new result
|
124
|
+
#puts "Error with request: #{http_method.upcase}: #{href}"
|
125
|
+
raise Stormpath::Error.new error
|
126
|
+
end
|
117
127
|
|
118
|
-
|
128
|
+
if http_method == 'delete'
|
129
|
+
cache = cache_for href
|
130
|
+
cache.delete href if cache
|
131
|
+
return nil
|
132
|
+
end
|
119
133
|
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
134
|
+
if result['href']
|
135
|
+
cache_walk result
|
136
|
+
else
|
137
|
+
result
|
138
|
+
end
|
124
139
|
end
|
125
140
|
|
126
|
-
|
127
|
-
|
128
|
-
|
141
|
+
def cache_walk(resource)
|
142
|
+
assert_not_nil resource['href'], "resource must have 'href' property"
|
143
|
+
items = resource['items']
|
129
144
|
|
130
|
-
|
145
|
+
if items # collection resource
|
146
|
+
resource['items'] = items.map do |item|
|
147
|
+
cache_walk item
|
148
|
+
{ 'href' => item['href'] }
|
149
|
+
end
|
150
|
+
else # single resource
|
151
|
+
resource.each do |attr, value|
|
152
|
+
if value.is_a? Hash and value['href']
|
153
|
+
walked = cache_walk value
|
154
|
+
resource[attr] = { 'href' => value['href'] } if value["href"]
|
155
|
+
resource[attr]['items'] = walked['items'] if walked['items']
|
156
|
+
end
|
157
|
+
end
|
158
|
+
cache resource if resource.length > 1
|
159
|
+
end
|
160
|
+
resource
|
161
|
+
end
|
131
162
|
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
raise Stormpath::Error.new error
|
163
|
+
def cache(resource)
|
164
|
+
cache = cache_for resource['href']
|
165
|
+
cache.put resource['href'], resource if cache
|
136
166
|
end
|
137
167
|
|
138
|
-
|
139
|
-
|
140
|
-
cache.delete href if cache
|
141
|
-
return nil
|
168
|
+
def cache_for(href)
|
169
|
+
@cache_manager.get_cache(region_for href)
|
142
170
|
end
|
143
171
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
172
|
+
def region_for(href)
|
173
|
+
return nil unless href
|
174
|
+
if href.include? "/customData"
|
175
|
+
region = href.split('/')[-1]
|
176
|
+
else
|
177
|
+
region = href.split('/')[-2]
|
178
|
+
end
|
179
|
+
CACHE_REGIONS.include?(region) ? region : nil
|
148
180
|
end
|
149
|
-
end
|
150
181
|
|
151
|
-
|
152
|
-
|
153
|
-
|
182
|
+
def apply_default_request_headers(request)
|
183
|
+
request.http_headers.store 'Accept', 'application/json'
|
184
|
+
request.http_headers.store 'User-Agent', 'Stormpath-RubySDK/' + Stormpath::VERSION
|
154
185
|
|
155
|
-
|
156
|
-
|
157
|
-
cache_walk item
|
158
|
-
{ 'href' => item['href'] }
|
159
|
-
end
|
160
|
-
else # single resource
|
161
|
-
resource.each do |attr, value|
|
162
|
-
if value.is_a? Hash
|
163
|
-
walked = cache_walk value
|
164
|
-
resource[attr] = { 'href' => value['href'] }
|
165
|
-
resource[attr]['items'] = walked['items'] if walked['items']
|
166
|
-
end
|
186
|
+
if !request.body.nil? and request.body.length > 0
|
187
|
+
request.http_headers.store 'Content-Type', 'application/json'
|
167
188
|
end
|
168
|
-
cache resource if resource.length > 1
|
169
189
|
end
|
170
|
-
resource
|
171
|
-
end
|
172
|
-
|
173
|
-
def cache(resource)
|
174
|
-
cache = cache_for resource['href']
|
175
|
-
cache.put resource['href'], resource if cache
|
176
|
-
end
|
177
190
|
|
178
|
-
|
179
|
-
|
180
|
-
|
191
|
+
def save_resource(href, resource, return_type)
|
192
|
+
assert_not_nil resource, "resource argument cannot be null."
|
193
|
+
assert_not_nil return_type, "returnType class cannot be null."
|
194
|
+
assert_kind_of Stormpath::Resource::Base, resource, "resource argument must be instance of Stormpath::Resource::Base"
|
181
195
|
|
182
|
-
|
183
|
-
return nil unless href
|
184
|
-
region = href.split('/')[-2]
|
185
|
-
CACHE_REGIONS.include?(region) ? region : nil
|
186
|
-
end
|
196
|
+
q_href = qualify href
|
187
197
|
|
188
|
-
|
189
|
-
request.http_headers.store 'Accept', 'application/json'
|
190
|
-
request.http_headers.store 'User-Agent', 'Stormpath-RubySDK/' + Stormpath::VERSION
|
198
|
+
response = execute_request('post', q_href, MultiJson.dump(to_hash(resource)))
|
191
199
|
|
192
|
-
|
193
|
-
request.http_headers.store 'Content-Type', 'application/json'
|
200
|
+
instantiate return_type, response.to_hash
|
194
201
|
end
|
195
|
-
end
|
196
|
-
|
197
|
-
def save_resource(href, resource, return_type)
|
198
|
-
assert_not_nil resource, "resource argument cannot be null."
|
199
|
-
assert_not_nil return_type, "returnType class cannot be null."
|
200
|
-
assert_kind_of Stormpath::Resource::Base, resource, "resource argument must be instance of Stormpath::Resource::Base"
|
201
|
-
|
202
|
-
q_href = if needs_to_be_fully_qualified href
|
203
|
-
qualify href
|
204
|
-
else
|
205
|
-
href
|
206
|
-
end
|
207
202
|
|
208
|
-
|
209
|
-
|
210
|
-
|
203
|
+
def get_base_url(base_url)
|
204
|
+
base_url || "https://" + DEFAULT_SERVER_HOST + "/v" + DEFAULT_API_VERSION.to_s
|
205
|
+
end
|
211
206
|
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
end
|
207
|
+
def to_hash(resource)
|
208
|
+
Hash.new.tap do |properties|
|
209
|
+
resource.get_dirty_property_names.each do |name|
|
210
|
+
property = resource.get_property name
|
217
211
|
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
212
|
+
# Special use case is with Custom Data, it's hashes should not be simplified
|
213
|
+
if property.kind_of?(Hash) and resource_not_custom_data resource, name
|
214
|
+
property = to_simple_reference name, property
|
215
|
+
end
|
222
216
|
|
223
|
-
|
224
|
-
property = to_simple_reference name, property
|
217
|
+
properties.store name, property
|
225
218
|
end
|
226
|
-
|
227
|
-
properties.store name, property
|
228
219
|
end
|
229
220
|
end
|
230
|
-
end
|
231
221
|
|
232
|
-
|
233
|
-
|
234
|
-
assert_true(
|
235
|
-
(hash.kind_of?(Hash) and !hash.empty? and hash.has_key?(href_prop_name)),
|
236
|
-
"Nested resource '#{property_name}' must have an 'href' property."
|
237
|
-
)
|
222
|
+
def to_simple_reference(property_name, hash)
|
223
|
+
assert_true hash.has_key?(HREF_PROP_NAME), "Nested resource '#{property_name}' must have an 'href' property."
|
238
224
|
|
239
|
-
|
225
|
+
href = hash[HREF_PROP_NAME]
|
226
|
+
|
227
|
+
{HREF_PROP_NAME => href}
|
228
|
+
end
|
229
|
+
|
230
|
+
def resource_not_custom_data resource, name
|
231
|
+
resource.class != Stormpath::Resource::CustomData and name != "customData"
|
232
|
+
end
|
240
233
|
|
241
|
-
{href_prop_name => href}
|
242
|
-
end
|
243
234
|
end
|