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 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
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in simple_crowd.gemspec
4
+ gemspec
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
+
@@ -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