active_zuora 1.3.0 → 2.6.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.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/.octopolo.yml +4 -0
- data/.soyuz.yml +13 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.markdown +41 -0
- data/Gemfile +4 -0
- data/MIT-LICENSE +20 -0
- data/README.md +191 -0
- data/Rakefile +9 -53
- data/TODO.md +2 -0
- data/active_zuora.gemspec +25 -59
- data/lib/active_zuora.rb +44 -12
- data/lib/active_zuora/amend.rb +43 -0
- data/lib/active_zuora/base.rb +84 -0
- data/lib/active_zuora/batch_subscribe.rb +53 -0
- data/lib/active_zuora/belongs_to_associations.rb +56 -0
- data/lib/active_zuora/billing_preview.rb +49 -0
- data/lib/active_zuora/collection_proxy.rb +38 -0
- data/lib/active_zuora/connection.rb +47 -0
- data/lib/active_zuora/fields.rb +129 -0
- data/lib/active_zuora/fields/array_field_decorator.rb +28 -0
- data/lib/active_zuora/fields/boolean_field.rb +12 -0
- data/lib/active_zuora/fields/date_field.rb +18 -0
- data/lib/active_zuora/fields/date_time_field.rb +19 -0
- data/lib/active_zuora/fields/decimal_field.rb +12 -0
- data/lib/active_zuora/fields/field.rb +76 -0
- data/lib/active_zuora/fields/integer_field.rb +11 -0
- data/lib/active_zuora/fields/object_field.rb +31 -0
- data/lib/active_zuora/fields/string_field.rb +11 -0
- data/lib/active_zuora/generate.rb +43 -0
- data/lib/active_zuora/generator.rb +244 -0
- data/lib/active_zuora/has_many_associations.rb +37 -0
- data/lib/active_zuora/has_many_proxy.rb +50 -0
- data/lib/active_zuora/lazy_attr.rb +52 -0
- data/lib/active_zuora/persistence.rb +172 -0
- data/lib/active_zuora/relation.rb +260 -0
- data/lib/active_zuora/scoping.rb +50 -0
- data/lib/active_zuora/subscribe.rb +42 -0
- data/lib/active_zuora/version.rb +3 -0
- data/lib/active_zuora/z_object.rb +21 -0
- data/spec/account_integration_spec.rb +41 -0
- data/spec/base_spec.rb +39 -0
- data/spec/belongs_to_associations_spec.rb +35 -0
- data/spec/collection_proxy_spec.rb +28 -0
- data/spec/connection_spec.rb +66 -0
- data/spec/fields/date_field_spec.rb +35 -0
- data/spec/has_many_integration_spec.rb +53 -0
- data/spec/lazy_attr_spec.rb +22 -0
- data/spec/spec_helper.rb +34 -0
- data/spec/subscribe_integration_spec.rb +344 -0
- data/spec/zobject_integration_spec.rb +104 -0
- data/wsdl/zuora.wsdl +1548 -0
- metadata +141 -53
- data/LICENSE +0 -202
- data/README.rdoc +0 -15
- data/VERSION +0 -1
- data/custom_fields.yml +0 -17
- data/lib/zuora/ZUORA.rb +0 -1398
- data/lib/zuora/ZUORADriver.rb +0 -128
- data/lib/zuora/ZUORAMappingRegistry.rb +0 -1488
- data/lib/zuora/ZuoraServiceClient.rb +0 -124
- data/lib/zuora/account.rb +0 -4
- data/lib/zuora/api.rb +0 -18
- data/lib/zuora/contact.rb +0 -4
- data/lib/zuora/rate_plan.rb +0 -4
- data/lib/zuora/rate_plan_data.rb +0 -4
- data/lib/zuora/subscribe_options.rb +0 -4
- data/lib/zuora/subscribe_request.rb +0 -4
- data/lib/zuora/subscribe_with_existing_account_request.rb +0 -4
- data/lib/zuora/subscription.rb +0 -4
- data/lib/zuora/subscription_data.rb +0 -4
- data/lib/zuora/zobject.rb +0 -52
- data/lib/zuora_client.rb +0 -181
- data/lib/zuora_interface.rb +0 -199
data/lib/active_zuora.rb
CHANGED
@@ -1,13 +1,45 @@
|
|
1
|
-
require '
|
2
|
-
require '
|
3
|
-
require '
|
4
|
-
require 'zuora/account'
|
5
|
-
require 'zuora/contact'
|
6
|
-
require 'zuora/rate_plan'
|
7
|
-
require 'zuora/rate_plan_data'
|
8
|
-
require 'zuora/subscribe_options'
|
9
|
-
require 'zuora/subscribe_request'
|
10
|
-
require 'zuora/subscribe_with_existing_account_request'
|
11
|
-
require 'zuora/subscription'
|
12
|
-
require 'zuora/subscription_data'
|
1
|
+
require 'savon'
|
2
|
+
require 'active_model'
|
3
|
+
require 'active_support/all'
|
13
4
|
|
5
|
+
require 'active_zuora/connection'
|
6
|
+
require 'active_zuora/generator'
|
7
|
+
require 'active_zuora/fields'
|
8
|
+
require 'active_zuora/belongs_to_associations'
|
9
|
+
require 'active_zuora/base'
|
10
|
+
require 'active_zuora/relation'
|
11
|
+
require 'active_zuora/scoping'
|
12
|
+
require 'active_zuora/persistence'
|
13
|
+
require 'active_zuora/has_many_proxy'
|
14
|
+
require 'active_zuora/has_many_associations'
|
15
|
+
require 'active_zuora/z_object'
|
16
|
+
require 'active_zuora/subscribe'
|
17
|
+
require 'active_zuora/amend'
|
18
|
+
require 'active_zuora/generate'
|
19
|
+
require 'active_zuora/billing_preview'
|
20
|
+
require 'active_zuora/batch_subscribe'
|
21
|
+
require 'active_zuora/collection_proxy'
|
22
|
+
require 'active_zuora/lazy_attr'
|
23
|
+
|
24
|
+
module ActiveZuora
|
25
|
+
|
26
|
+
# Setup configuration. None of this sends a request.
|
27
|
+
def self.configure(configuration)
|
28
|
+
# Set some sensible defaults with the savon SOAP client.
|
29
|
+
Savon.configure do |config|
|
30
|
+
config.log = HTTPI.log = configuration[:log] || false
|
31
|
+
config.log_level = configuration[:log_level] || :info
|
32
|
+
config.logger = configuration[:logger] if configuration[:logger]
|
33
|
+
config.logger.filter = configuration[:log_filters] || [:password, :SessionHeader]
|
34
|
+
config.raise_errors = true
|
35
|
+
end
|
36
|
+
# Create a default connection on Base
|
37
|
+
Base.connection = Connection.new(configuration)
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.generate_classes(options={})
|
41
|
+
generator = Generator.new(Base.connection.soap_client.wsdl.parser, options)
|
42
|
+
generator.generate_classes
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module ActiveZuora
|
2
|
+
module Amend
|
3
|
+
|
4
|
+
# This is meant to be included onto a AmendRequest class.
|
5
|
+
# Returns true/false on success.
|
6
|
+
# Result hash is stored in #result.
|
7
|
+
# If success, the ids will be set in the given amendments.
|
8
|
+
# If failure, errors will be present on object.
|
9
|
+
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
|
12
|
+
included do
|
13
|
+
include Base
|
14
|
+
attr_accessor :result
|
15
|
+
end
|
16
|
+
|
17
|
+
def amend
|
18
|
+
self.result = self.class.connection.request(:amend) do |soap|
|
19
|
+
soap.body do |xml|
|
20
|
+
build_xml(xml, soap,
|
21
|
+
:namespace => soap.namespace,
|
22
|
+
:element_name => :requests,
|
23
|
+
:force_type => true)
|
24
|
+
end
|
25
|
+
end[:amend_response][:results]
|
26
|
+
if result[:success]
|
27
|
+
[result[:amendment_ids]].flatten.compact.each_with_index do |id, i|
|
28
|
+
amendments[i].id = id
|
29
|
+
end
|
30
|
+
clear_changed_attributes
|
31
|
+
true
|
32
|
+
else
|
33
|
+
add_zuora_errors(result[:errors])
|
34
|
+
false
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def amend!
|
39
|
+
raise "Could not amend: #{errors.full_messages.join ', '}" unless amend
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'bigdecimal'
|
2
|
+
require 'bigdecimal/util'
|
3
|
+
|
4
|
+
module ActiveZuora
|
5
|
+
module Base
|
6
|
+
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
# This is the default connection object to use for all Base classes.
|
10
|
+
# Each individual Base class can overwrite it.
|
11
|
+
class << self
|
12
|
+
attr_accessor :connection
|
13
|
+
end
|
14
|
+
|
15
|
+
included do
|
16
|
+
include Fields
|
17
|
+
include ActiveModel::Validations
|
18
|
+
include BelongsToAssociations
|
19
|
+
class << self
|
20
|
+
attr_accessor :namespace
|
21
|
+
attr_writer :zuora_object_name
|
22
|
+
attr_writer :connection
|
23
|
+
end
|
24
|
+
delegate :namespace, :zuora_object_name, :to => 'self.class'
|
25
|
+
end
|
26
|
+
|
27
|
+
def xml_field_names
|
28
|
+
# Which field names should be rendered during build_xml.
|
29
|
+
# Choose only field names that have been changed.
|
30
|
+
# Make sure the order in fields is maintained.
|
31
|
+
field_names & changed.map(&:to_sym)
|
32
|
+
end
|
33
|
+
|
34
|
+
def build_xml(xml, soap, options={})
|
35
|
+
namespace = options.delete(:namespace) || self.namespace
|
36
|
+
qualifier = soap.namespace_by_uri(namespace)
|
37
|
+
custom_element_name = options.delete(:element_name)
|
38
|
+
element_name = custom_element_name || zuora_object_name
|
39
|
+
attributes = options.delete(:force_type) ?
|
40
|
+
{ "xsi:type" => "#{qualifier}:#{zuora_object_name}" } : {}
|
41
|
+
|
42
|
+
xml.tag!(qualifier, element_name.to_sym, attributes) do
|
43
|
+
xml_field_names.map { |field_name| get_field!(field_name) }.sort(&method(:fields_order)).each do |field|
|
44
|
+
field.build_xml(xml, soap, send(field.name), options)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def fields_order(a, b)
|
50
|
+
if send(a.name) == nil
|
51
|
+
send(b.name) == nil ? 0 : -1
|
52
|
+
elsif a.name.to_sym == :id
|
53
|
+
send(b.name) == nil ? 1 : -1
|
54
|
+
else
|
55
|
+
(b.name.to_sym == :id || send(b.name) == nil) ? 1 : 0
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def add_zuora_errors(zuora_errors)
|
60
|
+
return if zuora_errors.blank?
|
61
|
+
zuora_errors = [zuora_errors] unless zuora_errors.is_a?(Array)
|
62
|
+
zuora_errors.each { |error| errors.add(:base, error[:message].capitalize) }
|
63
|
+
end
|
64
|
+
|
65
|
+
module ClassMethods
|
66
|
+
|
67
|
+
def zuora_object_name
|
68
|
+
@zuora_object_name ||= self.name.split("::").last
|
69
|
+
end
|
70
|
+
|
71
|
+
def connection
|
72
|
+
@connection || Base.connection
|
73
|
+
end
|
74
|
+
|
75
|
+
def nested_class_name(unnested_class_name)
|
76
|
+
# This helper method will take a class name, and nest it inside
|
77
|
+
# the same module/class as self.
|
78
|
+
(name.split("::")[0..-2] << unnested_class_name).join("::")
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module ActiveZuora
|
2
|
+
module BatchSubscribe
|
3
|
+
|
4
|
+
# This is meant to be included onto the CollectionProxy class.
|
5
|
+
# Returns true if every SubscribeRequest was a success
|
6
|
+
# Returns false if any single subsciption Request fails
|
7
|
+
# Result hash of each subscribe request is stored in the Subscribe Request #result.
|
8
|
+
# If success, the subscription id and account id will be set in those objects.
|
9
|
+
# If failure, errors will be present on the request object(s) that had error(s).
|
10
|
+
|
11
|
+
extend ActiveSupport::Concern
|
12
|
+
|
13
|
+
included do
|
14
|
+
include Base
|
15
|
+
attr_accessor :result
|
16
|
+
end
|
17
|
+
|
18
|
+
def batch_subscribe
|
19
|
+
raise "object must be an ActiveZuora::CollectionProxy object instance" unless self.zuora_object_name == "CollectionProxy"
|
20
|
+
self.result = self.class.connection.request(:subscribe) do |soap|
|
21
|
+
soap.body do |xml|
|
22
|
+
inject(xml) do |memo, el|
|
23
|
+
el.build_xml(xml, soap,
|
24
|
+
:namespace => soap.namespace,
|
25
|
+
:element_name => :subscribes,
|
26
|
+
:force_type => true)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end[:subscribe_response][:result]
|
30
|
+
|
31
|
+
self.result = [result] unless result.is_a?(Array)
|
32
|
+
result.each_with_index do |result, i|
|
33
|
+
self.records[i].result = result
|
34
|
+
if result[:success]
|
35
|
+
#we assume order is maintained by zuora. is it?
|
36
|
+
self.records[i].account.id = result[:account_id]
|
37
|
+
self.records[i].subscription_data.subscription.id = result[:subscription_id]
|
38
|
+
self.records[i].clear_changed_attributes
|
39
|
+
@status = true
|
40
|
+
else
|
41
|
+
add_zuora_errors(result[:errors])
|
42
|
+
@status = false
|
43
|
+
end
|
44
|
+
end
|
45
|
+
@status
|
46
|
+
end
|
47
|
+
|
48
|
+
def batch_subscribe!
|
49
|
+
raise "Could not batch subscribe: #{errors.full_messages.join ', '}" unless batch_subscribe
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module ActiveZuora
|
2
|
+
module BelongsToAssociations
|
3
|
+
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
|
8
|
+
def belongs_to(item, options={})
|
9
|
+
class_name = options[:class_name] || nested_class_name(item.to_s.camelize)
|
10
|
+
foreign_key = options[:foreign_key] || :"#{item}_id"
|
11
|
+
# Add the field if it doesn't already exist.
|
12
|
+
field foreign_key, :string unless field? foreign_key
|
13
|
+
ivar = "@#{item}"
|
14
|
+
loaded_ivar = "@#{item}_loaded"
|
15
|
+
# Define the methods on an included module, so we can override
|
16
|
+
# them using super.
|
17
|
+
generated_attribute_methods.module_eval do
|
18
|
+
define_method(item) do
|
19
|
+
# Return the object if it was already loaded.
|
20
|
+
if instance_variable_get(loaded_ivar)
|
21
|
+
return instance_variable_get(ivar)
|
22
|
+
else
|
23
|
+
# Otherwise find it.
|
24
|
+
record = class_name.constantize.find self.send(foreign_key)
|
25
|
+
send("#{item}=", record)
|
26
|
+
record
|
27
|
+
end
|
28
|
+
end
|
29
|
+
define_method("#{item}=") do |record|
|
30
|
+
instance_variable_set(loaded_ivar, true)
|
31
|
+
instance_variable_set(ivar, record)
|
32
|
+
# Set the foreign key id attribute as well.
|
33
|
+
write_attribute(foreign_key, record.try(:id))
|
34
|
+
record
|
35
|
+
end
|
36
|
+
redefine_method("#{foreign_key}=") do |item_id|
|
37
|
+
item_id = write_attribute(foreign_key, item_id)
|
38
|
+
# Unload the object if the id is different.
|
39
|
+
if send("#{item}_loaded?") && send(item).id != item_id
|
40
|
+
instance_variable_set(loaded_ivar, false)
|
41
|
+
end
|
42
|
+
item_id
|
43
|
+
end
|
44
|
+
define_method("#{item}_loaded?") do
|
45
|
+
instance_variable_get(loaded_ivar) || false
|
46
|
+
end
|
47
|
+
define_method("reload_#{item}") do
|
48
|
+
instance_variable_set(loaded_ivar, false)
|
49
|
+
send(item)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module ActiveZuora
|
2
|
+
module BillingPreview
|
3
|
+
|
4
|
+
# This is meant to be included onto an BillingPreviewRequest class.
|
5
|
+
# Returns a BillingPreviewResponse object on success.
|
6
|
+
# Result hash is stored in #result.
|
7
|
+
# If failure, errors will be present on object.
|
8
|
+
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
|
11
|
+
included do
|
12
|
+
include Base
|
13
|
+
attr_accessor :result
|
14
|
+
end
|
15
|
+
|
16
|
+
def billing_preview
|
17
|
+
self.result = self.class.connection.request(:billing_preview) do |soap|
|
18
|
+
soap.body do |xml|
|
19
|
+
build_xml(xml, soap,
|
20
|
+
:namespace => soap.namespace,
|
21
|
+
:element_name => :requests,
|
22
|
+
:force_type => true)
|
23
|
+
end
|
24
|
+
end[:billing_preview_response][:results]
|
25
|
+
|
26
|
+
if result[:success]
|
27
|
+
clear_changed_attributes
|
28
|
+
filtered_invoice_items = self.result[:invoice_item].map do |invoice_item|
|
29
|
+
#Filter out data in the return value that are not valid invoice item fields such as
|
30
|
+
# :"@xmlns:ns2"=>"http://object.api.zuora.com/",
|
31
|
+
# :"@xmlns:xsi"=>"http://www.w3.org/2001/XMLSchema-instance",
|
32
|
+
# :"@xsi:type"=>"ns2:InvoiceItem"
|
33
|
+
invoice_item.select{|key, v| ActiveZuora::InvoiceItem.field_names.include?(key)}
|
34
|
+
end
|
35
|
+
ActiveZuora::BillingPreviewResult.new(self.result.merge(invoice_item: filtered_invoice_items))
|
36
|
+
else
|
37
|
+
add_zuora_errors(result[:errors])
|
38
|
+
false
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def billing_preview!
|
43
|
+
billing_preview.tap do |preview|
|
44
|
+
raise "Could not billing preview: #{errors.full_messages.join ', '}" unless preview
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module ActiveZuora
|
2
|
+
class CollectionProxy
|
3
|
+
|
4
|
+
include ZObject
|
5
|
+
include Enumerable
|
6
|
+
include BatchSubscribe
|
7
|
+
|
8
|
+
attr_reader :records, :zobject_class
|
9
|
+
|
10
|
+
def initialize(ary = [])
|
11
|
+
unless ary.empty?
|
12
|
+
raise "objects in collection must be ActiveZuora object instances" unless class_names = ary.map{|object| object.zuora_object_name}.uniq
|
13
|
+
raise "objects in collection must be ActiveZuora object instances of the same class" unless class_names.length == 1
|
14
|
+
@zobject_class = class_names.first
|
15
|
+
end
|
16
|
+
@records = ary
|
17
|
+
end
|
18
|
+
|
19
|
+
def add object
|
20
|
+
raise "object must be an ActiveZuora object instance" unless object.zuora_object_name
|
21
|
+
if records.empty?
|
22
|
+
@zobject_class = object.zuora_object_name
|
23
|
+
else
|
24
|
+
raise "object must be must be ActiveZuora object instances of the same class as other elements in the Collection" unless object.zuora_object_name == zobject_class
|
25
|
+
end
|
26
|
+
@records.push object
|
27
|
+
end
|
28
|
+
|
29
|
+
def each
|
30
|
+
records.each { |r| yield r }
|
31
|
+
end
|
32
|
+
|
33
|
+
def empty?
|
34
|
+
records.empty?
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module ActiveZuora
|
2
|
+
class Connection
|
3
|
+
|
4
|
+
attr_reader :soap_client
|
5
|
+
attr_accessor :custom_header
|
6
|
+
|
7
|
+
WSDL = File.expand_path('../../../wsdl/zuora.wsdl', __FILE__)
|
8
|
+
|
9
|
+
def initialize(configuration={})
|
10
|
+
# Store login credentials and create SOAP client.
|
11
|
+
@username = configuration[:username]
|
12
|
+
@password = configuration[:password]
|
13
|
+
@soap_client = Savon::Client.new do
|
14
|
+
wsdl.document = configuration[:wsdl] || WSDL
|
15
|
+
http.proxy = configuration[:http_proxy] if configuration[:http_proxy]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def login
|
20
|
+
# Returns a session_id upon success, raises an exception on failure.
|
21
|
+
# Instance variables aren't available within the soap request block.
|
22
|
+
body = { :username => @username, :password => @password }
|
23
|
+
header = @custom_header
|
24
|
+
@soap_client.request(:login) do
|
25
|
+
soap.body = body
|
26
|
+
soap.header = header
|
27
|
+
end[:login_response][:result][:session]
|
28
|
+
end
|
29
|
+
|
30
|
+
def request(*args, &block)
|
31
|
+
# instance variables aren't available within the soap request block for some reason.
|
32
|
+
header = { 'SessionHeader' => { 'session' => @session_id } }
|
33
|
+
header.merge!(@custom_header) if @custom_header
|
34
|
+
|
35
|
+
@soap_client.request(*args) do
|
36
|
+
soap.header = header
|
37
|
+
yield(soap)
|
38
|
+
end
|
39
|
+
rescue Savon::SOAP::Fault => exception
|
40
|
+
# Catch invalid sessions, and re-issue the request.
|
41
|
+
raise unless exception.message =~ /INVALID_SESSION/
|
42
|
+
@session_id = login
|
43
|
+
request(*args, &block)
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require 'active_zuora/fields/field'
|
2
|
+
require 'active_zuora/fields/boolean_field'
|
3
|
+
require 'active_zuora/fields/date_field'
|
4
|
+
require 'active_zuora/fields/date_time_field'
|
5
|
+
require 'active_zuora/fields/decimal_field'
|
6
|
+
require 'active_zuora/fields/integer_field'
|
7
|
+
require 'active_zuora/fields/object_field'
|
8
|
+
require 'active_zuora/fields/string_field'
|
9
|
+
require 'active_zuora/fields/array_field_decorator'
|
10
|
+
|
11
|
+
module ActiveZuora
|
12
|
+
module Fields
|
13
|
+
|
14
|
+
extend ActiveSupport::Concern
|
15
|
+
|
16
|
+
included do
|
17
|
+
include ActiveModel::Dirty
|
18
|
+
delegate :fields, :field_names, :field?, :get_field, :get_field!,
|
19
|
+
:default_attributes, :to => 'self.class'
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(attributes={})
|
23
|
+
# Start with defaults, and override those with the given attributes.
|
24
|
+
self.attributes = default_attributes.merge(attributes)
|
25
|
+
end
|
26
|
+
|
27
|
+
def attributes
|
28
|
+
# A requirement of ActiveModel::Attributes.
|
29
|
+
# Hash must use string keys.
|
30
|
+
attributes = {}
|
31
|
+
fields.each { |field| attributes[field.name] = send(field.name) }
|
32
|
+
attributes
|
33
|
+
end
|
34
|
+
|
35
|
+
def attributes=(attributes)
|
36
|
+
attributes.each { |key, value| send("#{key}=", value) }
|
37
|
+
end
|
38
|
+
|
39
|
+
def untracked_attributes=(attributes)
|
40
|
+
# Loads attributes without tracking dirt.
|
41
|
+
self.attributes = attributes
|
42
|
+
clear_changed_attributes
|
43
|
+
attributes
|
44
|
+
end
|
45
|
+
|
46
|
+
def write_attribute(name, value)
|
47
|
+
field = get_field!(name)
|
48
|
+
value = field.type_cast(value)
|
49
|
+
attribute_will_change!(name) if value != send(name)
|
50
|
+
instance_variable_set("@#{name}", value)
|
51
|
+
value
|
52
|
+
end
|
53
|
+
|
54
|
+
def clear_changed_attributes
|
55
|
+
if ActiveSupport.version.to_s.to_f >= 5.2
|
56
|
+
clear_changes_information
|
57
|
+
else
|
58
|
+
changed_attributes.clear
|
59
|
+
end
|
60
|
+
|
61
|
+
# If we have any fields that are also Base objects,
|
62
|
+
# clear their attributes as well.
|
63
|
+
fields.each { |field| field.clear_changed_attributes(send(field.name)) }
|
64
|
+
end
|
65
|
+
|
66
|
+
module ClassMethods
|
67
|
+
|
68
|
+
def fields_by_name
|
69
|
+
# { :field_name_symbol => field_object }
|
70
|
+
@fields ||= {}
|
71
|
+
end
|
72
|
+
|
73
|
+
def fields
|
74
|
+
fields_by_name.values
|
75
|
+
end
|
76
|
+
|
77
|
+
def field_names
|
78
|
+
fields_by_name.keys
|
79
|
+
end
|
80
|
+
|
81
|
+
def field?(name)
|
82
|
+
fields_by_name.key?(name.to_sym)
|
83
|
+
end
|
84
|
+
|
85
|
+
def add_field(name, field)
|
86
|
+
fields_by_name[name.to_sym] = field
|
87
|
+
# Define the setters, getters, and changed helpers.
|
88
|
+
field.define_instance_methods(self)
|
89
|
+
end
|
90
|
+
|
91
|
+
def get_field(name)
|
92
|
+
fields_by_name[name.to_sym]
|
93
|
+
end
|
94
|
+
|
95
|
+
def get_field!(name)
|
96
|
+
get_field(name) || raise(ArgumentError.new("No field in #{self} named #{name}"))
|
97
|
+
end
|
98
|
+
|
99
|
+
def field(name, type, options={})
|
100
|
+
# Check if this field should be an array, don't pass
|
101
|
+
# this option down to the field.
|
102
|
+
field_is_array = options.delete(:array) || false
|
103
|
+
# Create and register the field.
|
104
|
+
field = case type
|
105
|
+
when :string then StringField.new(name, namespace, options)
|
106
|
+
when :boolean then BooleanField.new(name, namespace, options)
|
107
|
+
when :integer then IntegerField.new(name, namespace, options)
|
108
|
+
when :decimal then DecimalField.new(name, namespace, options)
|
109
|
+
when :date then DateField.new(name, namespace, options)
|
110
|
+
when :datetime then DateTimeField.new(name, namespace, options)
|
111
|
+
when :object
|
112
|
+
class_name = options[:class_name] || nested_class_name(name.to_s.camelize)
|
113
|
+
ObjectField.new(name, namespace, class_name, options)
|
114
|
+
else
|
115
|
+
ArgumentError.new "Unknown field type: #{type}"
|
116
|
+
end
|
117
|
+
field = ArrayFieldDecorator.new(field) if field_is_array
|
118
|
+
add_field(name, field)
|
119
|
+
end
|
120
|
+
|
121
|
+
def default_attributes
|
122
|
+
default_attributes = {}
|
123
|
+
fields.each { |field| default_attributes[field.name] = field.default }
|
124
|
+
default_attributes
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|