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