zuora-ruby 0.1.0 → 0.2.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.
@@ -3,58 +3,52 @@
3
3
  module Zuora
4
4
  module Models
5
5
  class CardHolder
6
- include ActiveModel::Model
7
-
8
- ATTRIBUTES = :card_holder_name,
9
- :address_line_1,
10
- :address_line_2,
11
- :city,
12
- :state,
13
- :zip_code,
14
- :country,
15
- :phone,
16
- :email
17
-
18
- attr_accessor(*ATTRIBUTES)
19
-
20
- def attributes
21
- ATTRIBUTES
22
- end
23
-
24
- validates :card_holder_name,
25
- :address_line_1,
26
- :city,
27
- :state,
28
- :zip_code,
29
- :country,
30
- presence: true
31
-
32
- validates :card_holder_name,
33
- length: { maximum: 50 }
34
-
35
- validates :address_line_1,
36
- length: { maximum: 255 }
37
-
38
- validates :address_line_2,
39
- length: { maximum: 255 },
40
- allow_nil: true
41
-
42
- validates :city,
43
- length: { maximum: 40 }
44
-
45
- validates :state,
46
- inclusion: { in: Zuora::STATE_ABBREVIATIONS }
47
-
48
- validates :zip_code,
49
- length: { maximum: 20 }
50
-
51
- validates :phone,
52
- length: { maximum: 20 },
53
- allow_nil: true
54
-
55
- validates :email,
56
- length: { maximum: 80 },
57
- allow_nil: true
6
+ include DirtyValidAttr
7
+
8
+ dirty_valid_attr :card_holder_name,
9
+ type: String,
10
+ required?: true,
11
+ valid?: max_length(50)
12
+
13
+ dirty_valid_attr :address_line_1,
14
+ type: String,
15
+ required?: true,
16
+ valid?: max_length(255)
17
+
18
+ dirty_valid_attr :address_line_2,
19
+ type: String,
20
+ valid?: max_length(255)
21
+
22
+ dirty_valid_attr :city,
23
+ type: String,
24
+ required?: true,
25
+ valid?: max_length(40)
26
+
27
+ dirty_valid_attr :state,
28
+ type: String,
29
+ required?: true,
30
+ valid?: one_of(Zuora::STATE_ABBREVIATIONS)
31
+
32
+ dirty_valid_attr :zip_code,
33
+ type: String,
34
+ required?: true,
35
+ valid?: max_length(20)
36
+
37
+ dirty_valid_attr :country,
38
+ type: String,
39
+ required?: true,
40
+ valid?: max_length(50)
41
+
42
+ dirty_valid_attr :phone,
43
+ type: String,
44
+ required?: true,
45
+ valid?: max_length(20)
46
+
47
+ dirty_valid_attr :email,
48
+ type: String,
49
+ required?: true
50
+
51
+ alias_method :initialize, :initialize_attributes!
58
52
  end
59
53
  end
60
54
  end
@@ -1,44 +1,71 @@
1
- # encoding: utf-8
2
-
3
1
  module Zuora
4
2
  module Models
5
3
  class Contact
6
- include ActiveModel::Model
7
-
8
- ATTRIBUTES = :address_1,
9
- :address_2,
10
- :city,
11
- :country,
12
- :county,
13
- :fax,
14
- :first_name,
15
- :home_phone,
16
- :last_name,
17
- :mobile_phone,
18
- :nickname,
19
- :other_phone,
20
- :other_phone_type,
21
- :personal_email,
22
- :zip_code,
23
- :state,
24
- :tax_region,
25
- :work_email,
26
- :work_phone
27
-
28
- attr_accessor(*ATTRIBUTES)
29
-
30
- def attributes
31
- ATTRIBUTES
32
- end
33
-
34
- validates :first_name,
35
- :last_name,
36
- :country,
37
- presence: true
38
-
39
- validates :first_name,
40
- :last_name,
41
- length: { maximum: 100 }
4
+ include DirtyValidAttr
5
+
6
+ dirty_valid_attr :address_1,
7
+ type: String,
8
+ required?: true
9
+
10
+ dirty_valid_attr :address_2,
11
+ type: String
12
+
13
+ dirty_valid_attr :city,
14
+ type: String
15
+
16
+ dirty_valid_attr :country,
17
+ type: String
18
+
19
+ dirty_valid_attr :county,
20
+ type: String
21
+
22
+ dirty_valid_attr :fax,
23
+ type: String
24
+
25
+ dirty_valid_attr :home_phone,
26
+ type: String
27
+
28
+ dirty_valid_attr :first_name,
29
+ type: String,
30
+ required?: true,
31
+ valid?: max_length(100)
32
+
33
+ dirty_valid_attr :last_name,
34
+ type: String,
35
+ required?: true,
36
+ valid?: max_length(100)
37
+
38
+ dirty_valid_attr :mobile_phone,
39
+ type: String
40
+
41
+ dirty_valid_attr :nickname,
42
+ type: String
43
+
44
+ dirty_valid_attr :other_phone,
45
+ type: String
46
+
47
+ dirty_valid_attr :other_phone_type,
48
+ type: String
49
+
50
+ dirty_valid_attr :personal_email,
51
+ type: String
52
+
53
+ dirty_valid_attr :state,
54
+ type: String
55
+
56
+ dirty_valid_attr :tax_region,
57
+ type: String
58
+
59
+ dirty_valid_attr :work_email,
60
+ type: String
61
+
62
+ dirty_valid_attr :work_phone,
63
+ type: String
64
+
65
+ dirty_valid_attr :zip_code,
66
+ type: String
67
+
68
+ alias_method :initialize, :initialize_attributes!
42
69
  end
43
70
  end
44
71
  end
@@ -0,0 +1,192 @@
1
+ # Thanks to @jcarbo, @jwg2s, @jbender
2
+
3
+ # Rationale:
4
+
5
+ # Create classes of objects with a self-enforcing schemas, and the ability to
6
+ # track which fields have changed.
7
+
8
+ # Useful in modeling, validating, and serializing remote API endpoints,
9
+ # especially for PATCH updates where sending ambiguous nil values could
10
+ # override invisible defaults.
11
+
12
+ # Features:
13
+
14
+ # - attr_accessor like getter and setters, plus...
15
+ # - constructor checks required fields with support for multi-field
16
+ # validation predicates
17
+ # - per attribute coercion
18
+ # - type check
19
+ # - validation
20
+ # - dirty attribute tracking: what changed?
21
+ # (checks are all optional, and are applied in the above order)
22
+
23
+ require 'set'
24
+ require_relative 'validation_predicates'
25
+
26
+ ## Composite Types
27
+ module Boolean; end
28
+ class TrueClass; include Boolean; end
29
+ class FalseClass; include Boolean; end
30
+ # All roads lead to TrueClass
31
+ # true.is_a? Boolean => true (.is_a? Boolean => true ... )
32
+ # false.is_a? Boolean => true (.is_a? Boolean => true ... )
33
+
34
+ module DirtyValidAttr
35
+ def self.included(base)
36
+ base.include InstanceMethods
37
+ base.extend ClassMethods
38
+ base.extend ValidationPredicates
39
+ end
40
+
41
+ module InstanceMethods
42
+ attr_accessor :changed_attributes
43
+ attr_accessor :attributes
44
+
45
+ def fail_validation!(attr, value)
46
+ message = "Invalid value for: attr: #{attr} - value: #{value}}"
47
+ fail message
48
+ end
49
+
50
+ def fail_type!(attr, value, type)
51
+ fail %(Invalid type for: attr: #{attr}
52
+ - value: #{value}
53
+ - is: #{value.class}
54
+ - should be: #{type})
55
+ end
56
+
57
+ def coerce_value(coerce, value)
58
+ return coerce.call(value)
59
+ rescue
60
+ throw "Unable to coerce #{value}"
61
+ end
62
+ end
63
+
64
+ module ClassMethods
65
+ attr_accessor :attr_definitions
66
+
67
+ # @param [symbol] attr - attribute name
68
+ # @param [Hash] options - {[Class] type - checked using .is_a?, optional
69
+ # [Proc] valid? - predicate fn, optional
70
+ # [Proc] coerce - coercion fn, optional
71
+ # [Boolean] required? - default: nil (falsy)
72
+ def dirty_valid_attr(attr, options = {})
73
+ upsert_attr_definition! attr, options
74
+
75
+ # Setter
76
+ define_setter attr, options
77
+
78
+ # Getter
79
+ attr_reader attr
80
+ end
81
+
82
+ private
83
+
84
+ # Defines setter method for instance (plus validation)
85
+ # @param [Object] attr
86
+ # @param [Object] options
87
+ def define_setter(attr, options = {})
88
+ define_method("#{attr}=") do |value|
89
+ self.changed_attributes ||= Set.new
90
+
91
+ type, validation, coerce = options.values_at(:type, :valid?, :coerce)
92
+ value = coerce_value(coerce, value) if coerce
93
+ fail_validation!(attr, value) if validation && !validation.call(value)
94
+ fail_type!(attr, value, type) unless type && value.is_a?(type)
95
+
96
+ changed_attributes << attr
97
+ instance_variable_set "@#{attr}", value
98
+ end
99
+ end
100
+
101
+ # Upsert to class-level hash of {attr => options}
102
+ # @param [Sym] attr - attribtue name
103
+ # @param [Hash] options - see structure in dirty_valid_attr
104
+ # @sfx Updates class-level method
105
+ def upsert_attr_definition!(attr, options)
106
+ self.attr_definitions ||= {}
107
+ self.attr_definitions[attr] = options
108
+ end
109
+ end
110
+
111
+ # @param [Hash] attrs - initial attribute keys and values
112
+ # @return [Nil]
113
+ def initialize_attributes!(attrs = {})
114
+ required = required_attrs(attrs)
115
+ missing = required.keys - attrs.keys
116
+ fail "Missing required attrs: #{missing} " unless missing.empty?
117
+ attrs.each do |attr, v|
118
+ send("#{attr}=", v)
119
+ end
120
+ end
121
+
122
+ private
123
+
124
+ # @param [Hash] attrs - [Symbol] attr
125
+ # [String] value
126
+ # @return [Hash] - [String] attr
127
+ # [Hash] options (:required?, :valid?, :coerce)
128
+ def required_attrs(attrs = {})
129
+ # An attribute is determined to be 'missing' using the :required? key.
130
+ # if provided present in the attribute's definition.
131
+ #
132
+ # If :required? is callable, call it on attrs.
133
+ # This allows for expression logic like:
134
+ # require attr A if attr B == 3
135
+ # Else
136
+ # Use the boolean value true, or nil/false
137
+ # Defaults to :required? nil(false)
138
+ #
139
+ self.class.attr_definitions.select do |_attr, definition|
140
+ required = definition[:required?]
141
+ required.respond_to?(:call) ? required.call(attrs) : required
142
+ end
143
+ end
144
+ end
145
+
146
+ # class Account
147
+ # include DirtyValidAttr
148
+ #
149
+ # dirty_valid_attr :fuz,
150
+ # :type => String,
151
+ # valid: ->(attr) { attr == 'fuzz' },
152
+ # coerce => ->(attr) { attr.to_str }
153
+ #
154
+ # dirty_valid_attr :bizz,
155
+ # type: Fixnum,
156
+ # valid: ->(attr) { attr > 3 },
157
+ # :coerce => ->(attr){ attr.to_i }
158
+ #
159
+ # dirty_valid_attr :gaz,
160
+ # type: Fixnum
161
+ #
162
+ # def initialize(attrs)
163
+ # initialize_attributes!(attrs)
164
+ # end
165
+ # end
166
+
167
+ # Usage:
168
+
169
+ # Create a valid record
170
+ # > a = Account.new(:fuz => 'fuzz', :bizz => "4")
171
+ # <Account:0x007f8f0c9052c0 @changed_attributes=
172
+ # #<Set: {:fuz, :bizz}>, @fuz="fuzz", @bizz=4>
173
+
174
+ # Access changed attributes
175
+ # > a.changed_attributes
176
+ # #<Set: {:fuz, :bizz}>
177
+
178
+ # Basic coercion, validation, type checking:
179
+
180
+ # Raises on invalid value:
181
+ # > a = Account.new(:fuz => 'fuzz', :bizz => "2")
182
+ # RuntimeError: Invalid value for: attr: bizz - value: 2}
183
+
184
+ # Raises if unable to coerce
185
+ # > a = Account.new(:fuz => 'fuzz', :bizz => Set.new)
186
+ # RuntimeError: Unable to coerce #<Set:0x007f8f0b8588f0>
187
+
188
+ # Raises if invalid type
189
+ # RuntimeError: Invalid type for: attr: gaz
190
+ # - value: abc
191
+ # - is: String
192
+ # - should be: Fixnum
@@ -2,35 +2,35 @@ module Zuora
2
2
  module Models
3
3
  module PaymentMethods
4
4
  class CreditCard
5
- include ActiveModel::Model
5
+ include DirtyValidAttr
6
6
 
7
- ATTRIBUTES = :card_type,
8
- :card_number,
9
- :expiration_month,
10
- :expiration_year,
11
- :security_code
7
+ dirty_valid_attr :card_type,
8
+ type: String,
9
+ required?: true,
10
+ valid?: one_of(Zuora::CREDIT_CARD_TYPES)
12
11
 
13
- attr_accessor(*ATTRIBUTES)
12
+ dirty_valid_attr :card_number,
13
+ type: String,
14
+ required?: true
14
15
 
15
- def attributes
16
- ATTRIBUTES
17
- end
18
-
19
- validates :card_type,
20
- :card_number,
21
- :expiration_month,
22
- :expiration_year,
23
- :security_code,
24
- presence: true
16
+ dirty_valid_attr :expiration_month,
17
+ type: String,
18
+ required?: true,
19
+ valid?: one_of(Zuora::MONTHS)
25
20
 
26
- validates :card_type,
27
- inclusion: { in: Zuora::CREDIT_CARD_TYPES }
21
+ dirty_valid_attr :expiration_year,
22
+ type: String,
23
+ required?: true,
24
+ valid?: valid_year
28
25
 
29
- validates :expiration_month,
30
- inclusion: { in: Zuora::MONTHS }
26
+ dirty_valid_attr :security_code,
27
+ type: String,
28
+ required: true
31
29
 
32
- validates :expiration_year,
33
- length: { is: 4 }
30
+ # @param [Hash] attrs
31
+ def initialize(attrs)
32
+ initialize_attributes! attrs
33
+ end
34
34
  end
35
35
  end
36
36
  end