simple_crowd 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +26 -0
- data/Gemfile +4 -0
- data/README.md +41 -0
- data/Rakefile +21 -0
- data/lib/simple_crowd.rb +41 -0
- data/lib/simple_crowd/client.rb +295 -0
- data/lib/simple_crowd/crowd_entity.rb +199 -0
- data/lib/simple_crowd/crowd_error.rb +15 -0
- data/lib/simple_crowd/group.rb +11 -0
- data/lib/simple_crowd/mappers/soap_attributes.rb +13 -0
- data/lib/simple_crowd/mock_client.rb +102 -0
- data/lib/simple_crowd/user.rb +17 -0
- data/lib/simple_crowd/version.rb +3 -0
- data/simple_crowd.gemspec +57 -0
- data/test/crowd_config.yml.example +6 -0
- data/test/factories.rb +9 -0
- data/test/helper.rb +28 -0
- data/test/test_client.rb +331 -0
- data/test/test_simple_crowd.rb +22 -0
- data/test/test_user.rb +69 -0
- metadata +208 -0
data/.gitignore
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
## MAC OS
|
2
|
+
.DS_Store
|
3
|
+
|
4
|
+
## TEXTMATE
|
5
|
+
*.tmproj
|
6
|
+
tmtags
|
7
|
+
|
8
|
+
## EMACS
|
9
|
+
*~
|
10
|
+
\#*
|
11
|
+
.\#*
|
12
|
+
|
13
|
+
## VIM
|
14
|
+
*.swp
|
15
|
+
|
16
|
+
## PROJECT::GENERAL
|
17
|
+
coverage
|
18
|
+
rdoc
|
19
|
+
doc
|
20
|
+
pkg
|
21
|
+
.idea
|
22
|
+
.yardoc
|
23
|
+
.bundle
|
24
|
+
|
25
|
+
## PROJECT::SPECIFIC
|
26
|
+
test/crowd_config.yml
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
Simple Crowd (SOAP Client for Atlassian Crowd)
|
2
|
+
=====
|
3
|
+
|
4
|
+
A basic Atlassian Crowd Client based on their SOAP API.
|
5
|
+
All the standard API calls have been implemented to my knowledge as of Crowd 2.0
|
6
|
+
|
7
|
+
#### Some disclaimers:
|
8
|
+
|
9
|
+
|
10
|
+
- This gem was created before Atlassian created a REST API for Crowd which is why we implemented it in SOAP.
|
11
|
+
- This gem was created for Atlassian Crowd 2.0, but it should work with 2.2.
|
12
|
+
- We renamed "principal" to "user" in all the API calls for our convenience as this gem was initially created for internal use only.
|
13
|
+
- This gem is in use in production and has been fully tested, but we provide no guarantee or support if it does not work for you.
|
14
|
+
|
15
|
+
#### Service URL Options:
|
16
|
+
|
17
|
+
- :service_url => "http://localhost:8095/crowd/",
|
18
|
+
- :app_name => "crowd",
|
19
|
+
- :app_password => ""
|
20
|
+
|
21
|
+
Ex. `SimpleCrowd::Client.new(:service_url...)`
|
22
|
+
|
23
|
+
#### Some example calls:
|
24
|
+
|
25
|
+
- `client.authenticate_user("test@test.com", "testpassword")`
|
26
|
+
- returns token if authenticated or nil if not
|
27
|
+
- `client.find_user_with_attributes_by_name("test@test.com")`
|
28
|
+
- returns user with all custom attributes
|
29
|
+
- NOTE: find_user_by_name does not return custom attributes
|
30
|
+
- `client.is_valid_user_token?("SOMELARGECROWDTOKEN")`
|
31
|
+
- returns true or false
|
32
|
+
|
33
|
+
|
34
|
+
TODO
|
35
|
+
--
|
36
|
+
|
37
|
+
- Add support for arrays in custom attribute values
|
38
|
+
- Add exception/error types instead of throwing Simple::CrowdError for all errors
|
39
|
+
- Add support for group custom attributes (as of Crowd 2.1 or 2.2)
|
40
|
+
- Add more automated tests for groups and validation factors
|
41
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
require 'bundler'
|
5
|
+
Bundler::GemHelper.install_tasks
|
6
|
+
|
7
|
+
desc 'Run tests for InheritedResources.'
|
8
|
+
Rake::TestTask.new(:test) do |t|
|
9
|
+
t.pattern = 'test/**/*_test.rb'
|
10
|
+
t.verbose = true
|
11
|
+
end
|
12
|
+
|
13
|
+
desc 'Generate documentation for InheritedResources.'
|
14
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
15
|
+
rdoc.rdoc_dir = 'rdoc'
|
16
|
+
rdoc.title = 'InheritedResources'
|
17
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
18
|
+
rdoc.rdoc_files.include('README.rdoc')
|
19
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
20
|
+
end
|
21
|
+
|
data/lib/simple_crowd.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'savon'
|
2
|
+
require 'hashie'
|
3
|
+
require 'forwardable'
|
4
|
+
require 'simple_crowd/crowd_entity'
|
5
|
+
require 'simple_crowd/crowd_error'
|
6
|
+
require 'simple_crowd/user'
|
7
|
+
require 'simple_crowd/group'
|
8
|
+
require 'simple_crowd/client'
|
9
|
+
require 'simple_crowd/mappers/soap_attributes'
|
10
|
+
Dir['simple_crowd/mappers/*.rb'].each {|file| require File.basename(file, File.extname(file)) }
|
11
|
+
|
12
|
+
module SimpleCrowd
|
13
|
+
class << self
|
14
|
+
def config &config_block
|
15
|
+
config_block.call(options)
|
16
|
+
end
|
17
|
+
# SimpleCrowd default options
|
18
|
+
def options
|
19
|
+
@options ||= {
|
20
|
+
:service_url => "http://localhost:8095/crowd/",
|
21
|
+
:app_name => "crowd",
|
22
|
+
:app_password => ""
|
23
|
+
}
|
24
|
+
end
|
25
|
+
def soap_options base_options = self.options
|
26
|
+
@soap_options ||= base_options.merge({
|
27
|
+
:service_ns => "urn:SecurityServer",
|
28
|
+
:service_namespaces => {
|
29
|
+
'xmlns:auth' => 'http://authentication.integration.crowd.atlassian.com',
|
30
|
+
'xmlns:ex' => 'http://exception.integration.crowd.atlassian.com',
|
31
|
+
'xmlns:int' => 'http://soap.integration.crowd.atlassian.com',
|
32
|
+
'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema',
|
33
|
+
'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance'
|
34
|
+
}
|
35
|
+
})
|
36
|
+
@soap_options.merge!({:service_url => base_options[:service_url] + 'services/SecurityServer'})
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
|
@@ -0,0 +1,295 @@
|
|
1
|
+
module SimpleCrowd
|
2
|
+
class Client
|
3
|
+
def initialize options = {}
|
4
|
+
@options = SimpleCrowd.soap_options SimpleCrowd.options.merge(options)
|
5
|
+
|
6
|
+
# TODO: Fix error handling
|
7
|
+
# Errors do not contained Exception info so we'll handle the errors ourselves
|
8
|
+
# Savon::Response.raise_errors = false
|
9
|
+
# @client = Savon::Client.new @options[:service_url]
|
10
|
+
yield(@options) if block_given?
|
11
|
+
end
|
12
|
+
def app_token
|
13
|
+
@app_token ||= authenticate_application
|
14
|
+
end
|
15
|
+
attr_writer :app_token
|
16
|
+
|
17
|
+
def get_cookie_info
|
18
|
+
simple_soap_call :get_cookie_info
|
19
|
+
end
|
20
|
+
|
21
|
+
def get_granted_authorities
|
22
|
+
groups = simple_soap_call :get_granted_authorities
|
23
|
+
groups[:string] unless groups.nil?
|
24
|
+
end
|
25
|
+
|
26
|
+
def authenticate_application(name = @options[:app_name], password = @options[:app_password])
|
27
|
+
response = client.authenticate_application! do |soap|
|
28
|
+
prepare soap
|
29
|
+
soap.body = {:in0 => {
|
30
|
+
'auth:name' => name,
|
31
|
+
'auth:credential' => {'auth:credential' => password}
|
32
|
+
}.merge(no_validation_factors)}
|
33
|
+
end
|
34
|
+
raise CrowdError.new(response.soap_fault, response.to_hash[:fault]) if response.soap_fault?
|
35
|
+
response.to_hash[:authenticate_application_response][:out][:token]
|
36
|
+
end
|
37
|
+
|
38
|
+
# Authenticate user by name/pass and retrieve login token
|
39
|
+
# @return [String] user token
|
40
|
+
def authenticate_user name, password, factors = nil
|
41
|
+
if factors
|
42
|
+
factors = prepare_validation_factors(factors)
|
43
|
+
simple_soap_call :authenticate_principal, {'auth:application' => @options[:app_name], 'auth:name' => name,
|
44
|
+
'auth:credential' => {'auth:credential' => password},
|
45
|
+
'auth:validationFactors' => factors}
|
46
|
+
else
|
47
|
+
simple_soap_call :authenticate_principal_simple, name, password
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def create_user_token name
|
52
|
+
simple_soap_call :create_principal_token, name, nil
|
53
|
+
end
|
54
|
+
|
55
|
+
# Invalidate an existing user token (log out)
|
56
|
+
# NOTE: call will return true even if token is invalid
|
57
|
+
# @return [Boolean] success (does not guarantee valid token)
|
58
|
+
def invalidate_user_token token
|
59
|
+
simple_soap_call :invalidate_principal_token, token do |res|
|
60
|
+
!res.soap_fault? && res.to_hash.key?(:invalidate_principal_token_response)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def is_valid_user_token? token, factors = nil
|
65
|
+
factors = prepare_validation_factors(factors) unless factors.nil?
|
66
|
+
simple_soap_call :is_valid_principal_token, token, factors
|
67
|
+
end
|
68
|
+
|
69
|
+
def is_cache_enabled?
|
70
|
+
simple_soap_call :is_cache_enabled
|
71
|
+
end
|
72
|
+
|
73
|
+
def is_group_member? group, user
|
74
|
+
simple_soap_call :is_group_member, group, user
|
75
|
+
end
|
76
|
+
|
77
|
+
def find_group_by_name name
|
78
|
+
SimpleCrowd::Group.parse_from :soap, simple_soap_call(:find_group_by_name, name)
|
79
|
+
end
|
80
|
+
|
81
|
+
def find_all_group_names
|
82
|
+
(simple_soap_call :find_all_group_names)[:string]
|
83
|
+
end
|
84
|
+
|
85
|
+
def update_group group, description, active
|
86
|
+
simple_soap_call :update_group, group, description, active do |res|
|
87
|
+
!res.soap_fault? && res.to_hash.key?(:update_group_response)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def add_user_to_group user, group
|
92
|
+
simple_soap_call :add_principal_to_group, user, group do |res|
|
93
|
+
!res.soap_fault? && res.to_hash.key?(:add_principal_to_group_response)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def remove_user_from_group user, group
|
98
|
+
simple_soap_call :remove_principal_from_group, user, group do |res|
|
99
|
+
!res.soap_fault? && res.to_hash.key?(:remove_principal_from_group_response)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def reset_user_password name
|
104
|
+
simple_soap_call :reset_principal_credential, name do |res|
|
105
|
+
!res.soap_fault? && res.to_hash.key?(:reset_principal_credential_response)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def find_all_user_names
|
110
|
+
(simple_soap_call :find_all_principal_names)[:string]
|
111
|
+
end
|
112
|
+
|
113
|
+
def find_user_by_name name
|
114
|
+
SimpleCrowd::User.parse_from :soap, simple_soap_call(:find_principal_by_name, name) rescue nil
|
115
|
+
end
|
116
|
+
|
117
|
+
def find_user_with_attributes_by_name name
|
118
|
+
SimpleCrowd::User.parse_from :soap, simple_soap_call(:find_principal_with_attributes_by_name, name) rescue nil
|
119
|
+
end
|
120
|
+
|
121
|
+
def find_user_by_token token
|
122
|
+
SimpleCrowd::User.parse_from :soap, simple_soap_call(:find_principal_by_token, token) rescue nil
|
123
|
+
end
|
124
|
+
|
125
|
+
def find_username_by_token token
|
126
|
+
user = find_user_by_token token
|
127
|
+
user && user[:username]
|
128
|
+
end
|
129
|
+
|
130
|
+
# Exact email match
|
131
|
+
def find_user_by_email email
|
132
|
+
search_users_by_email(email).find{|u| u.email == email}
|
133
|
+
end
|
134
|
+
|
135
|
+
# Partial email match
|
136
|
+
def search_users_by_email email
|
137
|
+
search_users({'principal.email' => email})
|
138
|
+
end
|
139
|
+
|
140
|
+
def search_users restrictions
|
141
|
+
soap_restrictions = prepare_search_restrictions restrictions
|
142
|
+
users = simple_soap_call :search_principals, soap_restrictions rescue []
|
143
|
+
return [] if users.nil? || users[:soap_principal].nil?
|
144
|
+
users = users[:soap_principal].is_a?(Array) ? users[:soap_principal] : [users[:soap_principal]]
|
145
|
+
users.map{|u| SimpleCrowd::User.parse_from :soap, u}
|
146
|
+
end
|
147
|
+
|
148
|
+
def add_user user, credential
|
149
|
+
return if user.nil? || credential.nil?
|
150
|
+
[:email, :first_name, :last_name].each do |k|
|
151
|
+
user.send(:"#{k}=", "") if user.send(k).nil?
|
152
|
+
end
|
153
|
+
soap_user = user.map_to :soap
|
154
|
+
# We don't use these attributes when creating
|
155
|
+
soap_user.delete(:id)
|
156
|
+
soap_user.delete(:directory_id)
|
157
|
+
# Add blank attributes if missing
|
158
|
+
|
159
|
+
# Declare require namespaces
|
160
|
+
soap_user = soap_user.inject({}) {|hash, (k, v)| hash["int:#{k}"] = v;hash}
|
161
|
+
SimpleCrowd::User.parse_from :soap, simple_soap_call(:add_principal, soap_user, {'auth:credential' => credential, 'auth:encryptedCredential' => false})
|
162
|
+
end
|
163
|
+
|
164
|
+
def remove_user name
|
165
|
+
simple_soap_call :remove_principal, name do |res|
|
166
|
+
!res.soap_fault? && res.to_hash.key?(:remove_principal_response)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def update_user_credential user, credential, encrypted = false
|
171
|
+
simple_soap_call :update_principal_credential, user,
|
172
|
+
{'auth:credential' => credential, 'auth:encryptedCredential' => encrypted} do |res|
|
173
|
+
!res.soap_fault? && res.to_hash.key?(:update_principal_credential_response)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# Only supports single value attributes
|
178
|
+
# TODO: Allow value arrays
|
179
|
+
# @param user [String] name of user to update
|
180
|
+
# @param name [String] of attribute to update
|
181
|
+
# @param value [String] of attribute to update
|
182
|
+
def update_user_attribute user, name, value
|
183
|
+
return unless (name.is_a?(String) || name.is_a?(Symbol)) && (value.is_a?(String) || value.is_a?(Array))
|
184
|
+
soap_attr = SimpleCrowd::Mappers::SoapAttributes.produce({name => value})
|
185
|
+
simple_soap_call :update_principal_attribute, user, soap_attr['int:SOAPAttribute'][0] do |res|
|
186
|
+
!res.soap_fault? && res.to_hash.key?(:update_principal_attribute_response)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
alias_method :add_user_attribute, :update_user_attribute
|
190
|
+
|
191
|
+
# @param user [SimpleCrowd::User] dirty user to update
|
192
|
+
def update_user user
|
193
|
+
return unless user.dirty?
|
194
|
+
# Exclude non-attribute properties (only attributes can be updated in crowd)
|
195
|
+
attrs_to_update = user.dirty_attributes
|
196
|
+
return if attrs_to_update.empty?
|
197
|
+
|
198
|
+
attrs_to_update.each do |a|
|
199
|
+
prop = SimpleCrowd::User.property_by_name a
|
200
|
+
soap_prop = prop.maps[:soap].nil? ? prop : prop.maps[:soap]
|
201
|
+
self.update_user_attribute user.username, soap_prop, user.send(a)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
private
|
206
|
+
|
207
|
+
# Simplify the duplicated soap calls across methods
|
208
|
+
# @param [Symbol] action the soap action to call
|
209
|
+
# @param data the list of args to pass to the server as "in" args (in1, in2, etc.)
|
210
|
+
def simple_soap_call action, *data
|
211
|
+
# Take each arg and assign it to "in" keys for SOAP call starting with in1 (in0 is app token)
|
212
|
+
soap_args = data.inject({}){|hash, arg| hash[:"in#{hash.length + 1}"] = arg; hash }
|
213
|
+
# Ordered "in" keys ex. in1, in2, etc. for SOAP ordering
|
214
|
+
in_keys = soap_args.length ? (1..soap_args.length).collect {|v| :"in#{v}" } : []
|
215
|
+
# Make the SOAP call to the dynamic action
|
216
|
+
response = with_app_token do
|
217
|
+
client.send :"#{action}!" do |soap|
|
218
|
+
prepare soap
|
219
|
+
# Pass in all the args as "in" vars
|
220
|
+
soap.body = {:in0 => hash_authenticated_token}.merge(soap_args).merge({:order! => [:in0, *in_keys]})
|
221
|
+
end
|
222
|
+
end
|
223
|
+
# If a block is given then call it and pass in the response object, otherwise get the default out value
|
224
|
+
block_given? ? yield(response) : response.to_hash[:"#{action}_response"][:out]
|
225
|
+
end
|
226
|
+
|
227
|
+
def with_app_token retries = 1, &block
|
228
|
+
begin
|
229
|
+
Savon::Response.raise_errors = false
|
230
|
+
response = block.call
|
231
|
+
raise CrowdError.new(response.soap_fault, response.to_hash[:fault]) if response.soap_fault?
|
232
|
+
return response
|
233
|
+
rescue CrowdError => e
|
234
|
+
if retries && e.type?(:invalid_authorization_token_exception)
|
235
|
+
# Clear token to force a refresh
|
236
|
+
self.app_token = nil
|
237
|
+
retries -= 1
|
238
|
+
retry
|
239
|
+
end
|
240
|
+
raise
|
241
|
+
ensure
|
242
|
+
Savon::Response.raise_errors = true
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
# Generate new client on every request (Savon bug?)
|
247
|
+
def client
|
248
|
+
Savon::Client.new @options[:service_url]
|
249
|
+
end
|
250
|
+
|
251
|
+
# Setup soap object for request
|
252
|
+
def prepare soap
|
253
|
+
soap.namespace = @options[:service_ns]
|
254
|
+
soap.namespaces.merge! @options[:service_namespaces]
|
255
|
+
end
|
256
|
+
|
257
|
+
# Take Crowd SOAP attribute format and return a simple ruby hash
|
258
|
+
# @param attributes the soap attributes array
|
259
|
+
def process_soap_attributes attributes
|
260
|
+
soap = attributes[:soap_attribute]
|
261
|
+
(soap && soap.inject({}) {|hash, attr| hash[attr[:name].to_sym] = attr[:values][:string]; hash }) || {}
|
262
|
+
end
|
263
|
+
|
264
|
+
def map_group_hash group
|
265
|
+
attributes = process_soap_attributes group[:attributes]
|
266
|
+
supported_keys = attributes.keys & SimpleCrowd::Group.mapped_properties(:soap)
|
267
|
+
group = group.merge attributes.inject({}) {|map, (k, v)| map[k] = v if supported_keys.include? k; map}
|
268
|
+
group[:attributes] = attributes.inject({}) {|map, (k, v)| map[k] = v unless supported_keys.include? k; map}
|
269
|
+
group.delete :attributes if group[:attributes].empty?
|
270
|
+
SimpleCrowd::Group.new group
|
271
|
+
end
|
272
|
+
|
273
|
+
def prepare_validation_factors factors
|
274
|
+
{'auth:validationFactor' =>
|
275
|
+
factors.inject([]) {|arr, factor| arr << {'auth:name' => factor[0], 'auth:value' => factor[1]} }
|
276
|
+
}
|
277
|
+
end
|
278
|
+
|
279
|
+
def prepare_search_restrictions restrictions
|
280
|
+
{'int:searchRestriction' =>
|
281
|
+
restrictions.inject([]) {|arr, restrict| arr << {'int:name' => restrict[0], 'int:value' => restrict[1]}}
|
282
|
+
}
|
283
|
+
end
|
284
|
+
|
285
|
+
def hash_authenticated_token name = @options[:app_name], token = nil
|
286
|
+
token ||= app_token
|
287
|
+
{'auth:name' => name, 'auth:token' => token}
|
288
|
+
end
|
289
|
+
|
290
|
+
def no_validation_factors
|
291
|
+
{'auth:validationFactors' => {}, :attributes! => {'auth:validationFactors' => {'xsi:nil' => true}}}
|
292
|
+
end
|
293
|
+
|
294
|
+
end
|
295
|
+
end
|
@@ -0,0 +1,199 @@
|
|
1
|
+
module SimpleCrowd
|
2
|
+
class ExtendedProperty < Hashie::Dash
|
3
|
+
property :name
|
4
|
+
property :default
|
5
|
+
property :attribute
|
6
|
+
property :immutable
|
7
|
+
property :maps, :default => {}
|
8
|
+
property :mappers, :default => {}
|
9
|
+
def immutable?; @immutable; end
|
10
|
+
def is_attribute?; @attribute end
|
11
|
+
end
|
12
|
+
class CrowdEntity < Hashie::Mash
|
13
|
+
def initialize(data = {}, notused = nil)
|
14
|
+
self.class.properties.each do |prop|
|
15
|
+
self.send("#{prop.name}=", self.class.defaults[prop.name.to_sym])
|
16
|
+
end
|
17
|
+
attrs = data[:attributes].nil? ? [] : data[:attributes].keys
|
18
|
+
data.merge! data[:attributes] unless attrs.empty?
|
19
|
+
data.delete :attributes
|
20
|
+
data.each_pair do |att, value|
|
21
|
+
#ruby_att = att_to_ruby att
|
22
|
+
ruby_att = att
|
23
|
+
real_att = real_key_for ruby_att
|
24
|
+
(@attributes ||= []) << real_att if attrs.include?(att)
|
25
|
+
prop = self.class.property_by_name(real_att)
|
26
|
+
self.send("#{real_att}=", value) unless prop.nil?
|
27
|
+
self[real_att] = value if prop.nil?
|
28
|
+
end
|
29
|
+
# We just initialized the attributes so clear the dirty status
|
30
|
+
dirty_properties.clear
|
31
|
+
end
|
32
|
+
def self.property(property_name, options = {})
|
33
|
+
property_name = property_name.to_sym
|
34
|
+
|
35
|
+
maps = options.inject({}) {|map, (key, value)| map[$1.to_sym] = value.to_sym if key.to_s =~ /^map_(.*)$/; map }
|
36
|
+
mappers = options.inject({}) {|map, (key, value)| map[$1.to_sym] = value if key.to_s =~ /^mapper_(.*)$/; map }
|
37
|
+
options.reject! {|key, val| key.to_s =~ /^map_(.*)$/ || key.to_s =~ /^mapper_(.*)$/ }
|
38
|
+
(@properties ||= []) << ExtendedProperty.new({:name => property_name, :maps => maps, :mappers => mappers}.merge(options))
|
39
|
+
(@attributes ||= []) << property_name if options[:attribute]
|
40
|
+
|
41
|
+
class_eval <<-RUBY
|
42
|
+
def #{property_name}
|
43
|
+
self[:#{property_name}]
|
44
|
+
end
|
45
|
+
def #{property_name}=(val)
|
46
|
+
(dirty_properties << :#{property_name}).uniq! unless val == self[:#{property_name}]
|
47
|
+
self[:#{property_name}] = val
|
48
|
+
end
|
49
|
+
RUBY
|
50
|
+
|
51
|
+
maps.each_value do |v|
|
52
|
+
alias_method v, property_name
|
53
|
+
alias_method :"#{v}=", :"#{property_name}="
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.properties
|
58
|
+
properties = []
|
59
|
+
ancestors.each do |elder|
|
60
|
+
if elder.instance_variable_defined?("@properties")
|
61
|
+
properties << elder.instance_variable_get("@properties")
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
properties.reverse.flatten
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.property_by_name(property_name)
|
69
|
+
property_name = property_name.to_sym
|
70
|
+
properties.detect {|p| p.name == property_name || p.maps.value?(property_name)}
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.properties_by_name(property_name)
|
74
|
+
property_name = property_name.to_sym
|
75
|
+
properties.select {|p| p.name == property_name || p.maps.value?(property_name)}
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.property?(prop)
|
79
|
+
!property_by_name(prop.to_sym).nil?
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.defaults
|
83
|
+
properties.inject({}) {|hash, prop| hash[prop.name] = prop['default'] unless prop['default'].nil?; hash }
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.attribute_mappers hash = nil
|
87
|
+
@attribute_mappers ||= {:soap => SimpleCrowd::Mappers::SoapAttributes}
|
88
|
+
unless hash.nil?
|
89
|
+
@attribute_mappers.merge! hash if hash.is_a? Hash
|
90
|
+
end
|
91
|
+
@attribute_mappers
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.map_for type
|
95
|
+
type = type.to_sym
|
96
|
+
properties.inject({}) {|hash, prop| hash[prop.name] = prop.maps[type] unless prop.maps[type].nil?; hash }
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.map_to type, entity
|
100
|
+
map = map_for type
|
101
|
+
attrs = {}
|
102
|
+
out = entity.inject({}) do |hash, (key, val)|
|
103
|
+
key = key.to_sym
|
104
|
+
catch(:skip_prop) do
|
105
|
+
unless val.nil?
|
106
|
+
mapped_key = map[key].nil? ? key : map[key]
|
107
|
+
prop = property_by_name key
|
108
|
+
if prop.nil?
|
109
|
+
attrs[mapped_key] = val
|
110
|
+
throw :skip_prop
|
111
|
+
end
|
112
|
+
mapper = prop.mappers[type]
|
113
|
+
#val = val.inject({}) {|attrs, (k, v)| attrs[property_by_name(k).maps[type]]= v unless v.nil?; attrs} if key == :attributes
|
114
|
+
val = mapper.produce val unless mapper.nil?
|
115
|
+
if prop.attribute || entity.attributes_keys.include?(key)
|
116
|
+
attrs[mapped_key] = val
|
117
|
+
else
|
118
|
+
hash[mapped_key] = val
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
hash
|
123
|
+
end
|
124
|
+
out[:attributes] = attribute_mappers[type].produce attrs
|
125
|
+
out
|
126
|
+
end
|
127
|
+
|
128
|
+
def self.parse_from type, entity
|
129
|
+
entity[:attributes] = attribute_mappers[type].parse entity[:attributes]
|
130
|
+
parsed_entity = entity.inject({}) do |hash, (key, val)|
|
131
|
+
prop = property_by_name key
|
132
|
+
unless prop.nil?
|
133
|
+
mapper = prop.mappers[type]
|
134
|
+
val = mapper.parse val unless mapper.nil?
|
135
|
+
end
|
136
|
+
hash[key] = val
|
137
|
+
hash
|
138
|
+
end
|
139
|
+
self.new(parsed_entity)
|
140
|
+
end
|
141
|
+
|
142
|
+
def map_to type
|
143
|
+
self.class.map_to type, self
|
144
|
+
end
|
145
|
+
|
146
|
+
def attributes_keys
|
147
|
+
keys = []
|
148
|
+
self.class.ancestors.each do |elder|
|
149
|
+
if elder.instance_variable_defined?("@attributes")
|
150
|
+
keys << elder.instance_variable_get("@attributes")
|
151
|
+
end
|
152
|
+
end
|
153
|
+
keys << @attributes unless @attributes.nil?
|
154
|
+
keys.flatten.uniq
|
155
|
+
end
|
156
|
+
|
157
|
+
def attributes
|
158
|
+
self.inject({}) {|hash, (k, v)| hash[k] = v if attributes_keys.include?(k.to_sym); hash}
|
159
|
+
end
|
160
|
+
|
161
|
+
def dirty_properties
|
162
|
+
@dirty_properties ||= Array.new
|
163
|
+
end
|
164
|
+
|
165
|
+
def dirty_attributes
|
166
|
+
dirty_properties & attributes_keys
|
167
|
+
end
|
168
|
+
|
169
|
+
def dirty? prop = nil
|
170
|
+
prop.nil? ? !@dirty_properties.empty? : @dirty_properties.include?(prop)
|
171
|
+
end
|
172
|
+
|
173
|
+
def clean
|
174
|
+
@dirty_properties = Array.new
|
175
|
+
end
|
176
|
+
|
177
|
+
def update_with attrs
|
178
|
+
current_keys = attributes_keys
|
179
|
+
attrs.each_pair {|k, v| self.send(:"#{k}=", v) if current_keys.include?(k.to_sym) && v != self.send(k.to_sym)}
|
180
|
+
end
|
181
|
+
|
182
|
+
def []= key, val
|
183
|
+
prop = self.class.property_by_name key
|
184
|
+
(@attributes ||= []) << key if prop.nil?
|
185
|
+
super
|
186
|
+
end
|
187
|
+
|
188
|
+
private
|
189
|
+
def self.real_key_for att
|
190
|
+
p = property_by_name att
|
191
|
+
p.nil? ? att : p.name
|
192
|
+
end
|
193
|
+
def self.att_to_ruby att
|
194
|
+
att.to_s =~ /^[a-z]*([A-Z][^A-Z]*)*$/ ? att.to_s.underscore.to_sym : att.to_sym
|
195
|
+
end
|
196
|
+
def real_key_for att; self.class.real_key_for att; end
|
197
|
+
def att_to_ruby att; self.class.att_to_ruby att; end
|
198
|
+
end
|
199
|
+
end
|