sklik-api 0.0.16 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -8,7 +8,7 @@ class SklikApi
8
8
  :premiseMode, :premiseID
9
9
  ]
10
10
 
11
- include Object
11
+ include SklikObject
12
12
  =begin
13
13
  Example of input hash
14
14
  {
@@ -22,10 +22,23 @@ Example of input hash
22
22
 
23
23
  =end
24
24
 
25
- def initialize adgroup, args
25
+ def initialize args, deprecated_args = {}
26
+
27
+ #deprecated way to set up new adgroup!
28
+ if args.is_a?(SklikApi::Adgroup)
29
+ puts "DEPRECATION WARNING: Please update your code for SklikApi::Adtext.new(adgroup, args) to SklikApi::Adtext.new(args = {}) possible to add parent adgroup by adding :adgroup => your adgroup"
30
+ #set adgroup owner campaign
31
+ @adgroup = args
32
+ args = deprecated_args
33
+ #new way to set adgroups!
34
+ else
35
+ #set adgroup owner campaign
36
+ #if in input args there is pointer to parent campaign!
37
+ @adgroup = args.delete(:adgroup)
38
+ end
39
+ @args = args
40
+
26
41
  @adtext_data = nil
27
- #set adtext owner adgroup
28
- @adgroup = adgroup
29
42
 
30
43
  super args
31
44
  end
@@ -35,50 +48,106 @@ Example of input hash
35
48
  end
36
49
 
37
50
  def create_args
38
- raise ArgumentError, "Adtexts need's to know adgroup_id" unless @adgroup.args[:adgroup_id]
51
+ raise ArgumentError, "Adtexts need's to know adgroup_id" unless @args[:adgroup_id] || @adgroup.args[:adgroup_id]
39
52
  out = []
40
53
  #add campaign id to know where to create adgroup
41
- out << @adgroup.args[:adgroup_id]
54
+ out << @args[:adgroup_id] || @adgroup.args[:adgroup_id]
42
55
 
43
56
  #add adtext struct
44
- args = {}
45
- args[:creative1] = @args[:headline]
46
- args[:creative2] = @args[:description1]
47
- args[:creative3] = @args[:description2]
48
- args[:clickthruText] = @args[:display_url]
49
- args[:clickthruUrl] = @args[:url]
57
+ c_args = {}
58
+ c_args[:creative1] = @args[:headline]
59
+ c_args[:creative2] = @args[:description1]
60
+ c_args[:creative3] = @args[:description2]
61
+ c_args[:clickthruText] = @args[:display_url]
62
+ c_args[:clickthruUrl] = @args[:url]
63
+ c_args[:status] = status_for_update if status_for_update
50
64
 
51
65
  ADDITIONAL_FIELDS.each do |add_info|
52
66
  field_name = add_info.to_s.underscore.to_sym
53
- args[add_info] = @args[field_name] if @args[field_name]
67
+ c_args[add_info] = @args[field_name] if @args[field_name]
54
68
  end
55
69
 
56
- out << args
70
+ out << c_args
57
71
 
58
72
  #return output
59
73
  out
60
74
  end
61
75
 
62
- def self.find adgroup, args = {}
63
- out = []
64
- super(NAME, adgroup.args[:adgroup_id]).each do |adtext|
76
+ def update_args
77
+ out = []
78
+
79
+ #add campaign id on which will be performed update
80
+ out << @args[:adtext_id]
81
+
82
+ #prepare campaign struct
83
+ u_args = {}
84
+ u_args[:status] = status_for_update if status_for_update
85
+
86
+ out << u_args
87
+ ADDITIONAL_FIELDS.each do |add_info|
88
+ field_name = add_info.to_s.underscore.to_sym
89
+ u_args[add_info] = @args[field_name] if @args[field_name]
90
+ end
91
+
92
+ out
93
+ end
94
+
95
+ def self.get id
96
+ if adtext = super(NAME, id)
97
+ SklikApi::Adtext.new(process_sklik_data adtext)
98
+ else
99
+ nil
100
+ end
101
+ end
102
+
103
+ def self.find args, deprecated_args = {}
104
+ out = []
105
+
106
+ if args.is_a?(Integer)
107
+ return get args
108
+
109
+ #asking for adgroup deprecated way!
110
+ elsif args.is_a?(SklikApi::Adgroup)
111
+ puts "DEPRECATION WARNING: Please update your code for SklikApi::Adtext.find(adgroup, args) to SklikApi::Adtext.find(adgroup_id: 1234) possible to add parent adgroup by adding :adgroup => your adgroup"
112
+ adgroup_id = args.args[:adgroup_id]
113
+ args = deprecated_args
114
+
115
+ #asking for adgroup by hash with keyword_id
116
+ elsif args.is_a?(Hash) && args[:adtext_id]
117
+ if adtext = get(args[:adtext_id])
118
+ return [adtext]
119
+ else
120
+ return []
121
+ end
122
+
123
+ #asking for keyword by hash
124
+ else
125
+ adgroup_id = args[:adgroup_id]
126
+ end
127
+
128
+ return [] unless adgroup_id
129
+
130
+ super(NAME, adgroup_id).each do |adtext|
65
131
  if args[:adtext_id].nil? || (args[:adtext_id] && args[:adtext_id].to_i == adtext[:id].to_i)
66
- out << SklikApi::Adtext.new(
67
- adgroup,
68
- :adtext_id => adtext[:id],
69
- :headline => adtext[:creative1],
70
- :description1 => adtext[:creative2],
71
- :description2 => adtext[:creative3],
72
- :display_url =>adtext[:clickthruText],
73
- :url => adtext[:clickthruUrl],
74
- :name => adtext[:name],
75
- :status => fix_status(adtext)
76
- )
132
+ out << SklikApi::Adtext.new(process_sklik_data adtext)
77
133
  end
78
134
  end
79
135
  out
80
136
  end
81
137
 
138
+ def self.process_sklik_data adtext = {}
139
+ {
140
+ :adgroup_id => adtext[:groupId],
141
+ :adtext_id => adtext[:id],
142
+ :headline => adtext[:creative1],
143
+ :description1 => adtext[:creative2],
144
+ :description2 => adtext[:creative3],
145
+ :display_url =>adtext[:clickthruText],
146
+ :url => adtext[:clickthruUrl],
147
+ :status => fix_status(adtext)
148
+ }
149
+ end
150
+
82
151
  def self.fix_status adtext
83
152
  if adtext[:removed] == true
84
153
  return :stopped
@@ -99,14 +168,79 @@ Example of input hash
99
168
  end
100
169
  end
101
170
 
171
+ def self.get_current_status args = {}
172
+ raise ArgumentError, "Adtext_id is required" unless args[:adtext_id]
173
+ if adgroup = self.get(args[:adtext_id])
174
+ adgroup.args[:status]
175
+ else
176
+ raise ArgumentError, "Adtext by #{args.inspect} couldn't be found!"
177
+ end
178
+ end
179
+
180
+ def get_current_status
181
+ self.class.get_current_status :adtext_id => @args[:adtext_id]
182
+ end
183
+
184
+ def valid?
185
+ clear_errors
186
+ log_error "headline is required or too long" unless args[:headline] && args[:headline].size > 0 && args[:headline].size <= 35
187
+ log_error "description1 is required or too long" unless args[:description1] && args[:description1].size > 0 && args[:description1].size <= 45
188
+ log_error "description2 is required or too long" unless args[:description2] && args[:description2].size > 0 && args[:description2].size <= 45
189
+ log_error "display_url is required or too long" unless args[:display_url] && args[:display_url].size > 0 && args[:display_url].size <= 45
190
+ log_error "url is required" unless args[:url] && args[:url].size > 0 && args[:url].size <= 45
191
+ log_error "adgroup_id is required" unless args[:adgroup_id] || (@adgroup && @adgroup.args[:adgroup_id])
192
+ !errors.any?
193
+ end
194
+
195
+ def update args = {}
196
+ @args.merge!(args)
197
+ save
198
+ end
199
+
102
200
  def save
201
+ clear_errors
103
202
  if @args[:adtext_id] #do update
104
203
 
105
- else #do save
106
- #create adtext
107
- create
204
+ #get current status of campaign
205
+ before_status = get_current_status
206
+
207
+ #restore campaign before update
208
+ restore if before_status == :stopped
209
+
210
+ begin
211
+ update_object
212
+
213
+ rescue Exception => e
214
+ log_error e.message
215
+ end
216
+
217
+ #remove it if new status is stopped or status doesn't changed and before it was stopped
218
+ remove if (@args[:status] == :stopped) || (@args[:status].nil? && before_status == :stopped)
219
+
220
+ else #do save
221
+ @args[:adgroup_id] = @adgroup.args[:adgroup_id] if !@args[:adgroup_id] && @adgroup.args[:adgroup_id]
222
+
223
+ begin
224
+ #create adtext
225
+ create
226
+ rescue Exception => e
227
+ log_error e.message
228
+ #don't continue with creating campaign!
229
+ return false
230
+ end
231
+
232
+ #remove it if new status is stopped
233
+ remove if @args[:status] && @args[:status].to_s.to_sym == :stopped
108
234
  end
235
+
236
+ !errors.any?
109
237
  end
238
+
239
+ def log_error message
240
+ @adgroup.log_error "Adtext: #{@args[:headline]} -> #{message}" if @adgroup
241
+ errors << message
242
+ end
243
+
110
244
  end
111
245
  end
112
246
 
@@ -4,52 +4,109 @@ class SklikApi
4
4
 
5
5
  NAME = "keyword"
6
6
 
7
- include Object
7
+ ADDITIONAL_FIELDS = [
8
+ :cpc, :url
9
+ ]
10
+ ADDITIONAL_READ_FIELDS = [
11
+ :disabled, :cpc, :url, :minCpc
12
+ ]
13
+
14
+ include SklikObject
8
15
  =begin
9
16
  Example of input hash
10
17
  {
11
18
  :keyword => "\"some funny keyword\""
12
19
  }
13
20
  =end
14
-
15
- def initialize adgroup, args
21
+
22
+ def initialize args, deprecated_args = {}
23
+
24
+ #deprecated way to set up new adgroup!
25
+ if args.is_a?(SklikApi::Adgroup)
26
+ puts "DEPRECATION WARNING: Please update your code for SklikApi::Keyword.new(adgroup, args) to SklikApi::Keyword.new(args = {}) possible to add parent adgroup by adding :adgroup => your adgroup"
27
+ #set adgroup owner campaign
28
+ @adgroup = args
29
+ args = deprecated_args
30
+
31
+ #new way to set adgroups!
32
+ else
33
+ #set adgroup owner campaign
34
+ #if in input args there is pointer to parent campaign!
35
+ @adgroup = args.delete(:adgroup)
36
+ end
37
+ @args = args
38
+
16
39
  @keyword_data = nil
17
- #set keyword owner adgroup
18
- @adgroup = adgroup
19
40
 
20
41
  super args
21
42
  end
22
-
43
+
23
44
  def create_args
24
- raise ArgumentError, "Keyword need's to know adgroup_id" unless @adgroup.args[:adgroup_id]
45
+ raise ArgumentError, "Keyword need's to know adgroup_id" unless @args[:adgroup_id] || @adgroup.args[:adgroup_id]
25
46
  out = []
26
47
  #add campaign id to know where to create adgroup
27
- out << @adgroup.args[:adgroup_id]
28
-
48
+ out << @args[:adgroup_id] || @adgroup.args[:adgroup_id]
49
+
29
50
  #add adtext struct
30
- args = {}
31
- args[:name] = strip_match_type @args[:keyword]
32
- args[:matchType] = get_math_type @args[:keyword]
33
- out << args
34
-
51
+ c_args = {}
52
+ c_args[:name] = strip_match_type @args[:keyword]
53
+ c_args[:matchType] = get_math_type @args[:keyword]
54
+ #Currently not working :(
55
+ c_args[:status] = status_for_update if status_for_update
56
+
57
+ ADDITIONAL_FIELDS.each do |add_info|
58
+ field_name = add_info.to_s.underscore.to_sym
59
+ c_args[add_info] = @args[field_name] if @args[field_name]
60
+ end
61
+
62
+ if @args[:cpc]
63
+ c_args[:cpc] = (@args[:cpc] * 100).to_i
64
+ end
65
+
66
+
67
+ out << c_args
68
+
35
69
  #return output
36
70
  out
37
71
  end
38
-
72
+
73
+ def update_args
74
+ out = []
75
+
76
+ #add campaign id on which will be performed update
77
+ out << @args[:keyword_id]
78
+
79
+ #prepare campaign struct
80
+ u_args = {}
81
+ u_args[:status] = status_for_update if status_for_update
82
+
83
+ ADDITIONAL_FIELDS.each do |add_info|
84
+ field_name = add_info.to_s.underscore.to_sym
85
+ u_args[add_info] = @args[field_name] if @args[field_name]
86
+ end
87
+ if @args[:cpc]
88
+ u_args[:cpc] = (@args[:cpc] * 100).to_i
89
+ end
90
+ out << u_args
91
+
92
+ #return output
93
+ out
94
+ end
95
+
39
96
  def strip_match_type keyword
40
97
  keyword.gsub(/(\[|\]|\")/, "").gsub(/^-/, "")
41
98
  end
42
-
99
+
43
100
  def get_math_type keyword
44
- if /^-\[.*\]$/ =~ keyword
101
+ if /^-\[.*\]$/ =~ keyword
45
102
  return "negativeExact"
46
- elsif /^\[.*\]$/ =~ keyword
103
+ elsif /^\[.*\]$/ =~ keyword
47
104
  return "exact"
48
- elsif /^-\".*\"$/ =~ keyword
105
+ elsif /^-\".*\"$/ =~ keyword
49
106
  return "negativePhrase"
50
- elsif /^\".*\"$/ =~ keyword
107
+ elsif /^\".*\"$/ =~ keyword
51
108
  return "phrase"
52
- elsif /^-.*$/ =~ keyword
109
+ elsif /^-.*$/ =~ keyword
53
110
  return "negativeBroad"
54
111
  else
55
112
  return "broad"
@@ -67,22 +124,66 @@ Example of input hash
67
124
  else keyword
68
125
  end
69
126
  end
70
-
71
- def self.find adgroup, args = {}
127
+
128
+ def self.get id
129
+ if keyword = super(NAME, id)
130
+ SklikApi::Keyword.new(process_sklik_data keyword)
131
+ else
132
+ nil
133
+ end
134
+ end
135
+
136
+ def self.find args, deprecated_args = {}
72
137
  out = []
73
- super(NAME, adgroup.args[:adgroup_id]).each do |keyword|
138
+
139
+ if args.is_a?(Integer)
140
+ return get args
141
+
142
+ #asking for adgroup deprecated way!
143
+ elsif args.is_a?(SklikApi::Adgroup)
144
+ puts "DEPRECATION WARNING: Please update your code for SklikApi::Keyword.find(adgroup, args) to SklikApi::Keyword.find(adgroup_id: 1234) possible to add parent adgroup by adding :adgroup => your adgroup"
145
+ adgroup_id = args.args[:adgroup_id]
146
+ args = deprecated_args
147
+
148
+ #asking for adgroup by hash with keyword_id
149
+ elsif args.is_a?(Hash) && args[:keyword_id]
150
+ if keyword = get(args[:keyword_id])
151
+ return [keyword]
152
+ else
153
+ return []
154
+ end
155
+
156
+ #asking for keyword by hash
157
+ else
158
+ adgroup_id = args[:adgroup_id]
159
+ end
160
+
161
+ return [] unless adgroup_id
162
+
163
+ super(NAME, adgroup_id).each do |keyword|
74
164
  if args[:keyword_id].nil? || (args[:keyword_id] && args[:keyword_id].to_i == keyword[:id].to_i)
75
- out << SklikApi::Keyword.new(
76
- adgroup,
77
- :keyword_id => keyword[:id],
78
- :keyword => apply_math_type(keyword[:name], keyword[:matchType] ),
79
- :status => fix_status(keyword)
80
- )
165
+ out << SklikApi::Keyword.new(process_sklik_data keyword)
81
166
  end
82
167
  end
83
168
  out
84
169
  end
85
170
 
171
+ def self.process_sklik_data keyword = {}
172
+ out = {
173
+ :adgroup_id => keyword[:groupId],
174
+ :keyword_id => keyword[:id],
175
+ :keyword => apply_math_type(keyword[:name], keyword[:matchType] ),
176
+ :status => fix_status(keyword)
177
+ }
178
+ ADDITIONAL_READ_FIELDS.each do |add_info|
179
+ field_name = add_info.to_s.underscore.to_sym
180
+ out[field_name] = keyword[add_info] if keyword[add_info]
181
+ end
182
+ out[:cpc] = keyword[:cpc].to_f/100.0 if keyword[:cpc]
183
+
184
+ out
185
+ end
186
+
86
187
  def self.fix_status keyword
87
188
  if keyword[:removed] == true
88
189
  return :stopped
@@ -101,18 +202,71 @@ Example of input hash
101
202
  if @keyword_data
102
203
  @keyword_data
103
204
  else
104
- @keyword_data = {:keyword => @args[:keyword], :status => @args[:status]}
205
+ @keyword_data = @args end
206
+ end
207
+
208
+
209
+ def self.get_current_status args = {}
210
+ raise ArgumentError, "Keyword_id is required" unless args[:keyword_id]
211
+ if adgroup = self.get(args[:keyword_id])
212
+ adgroup.args[:status]
213
+ else
214
+ raise ArgumentError, "Keyword by #{args.inspect} couldn't be found!"
105
215
  end
106
216
  end
107
-
108
- def save
217
+
218
+ def get_current_status
219
+ self.class.get_current_status :keyword_id => @args[:keyword_id]
220
+ end
221
+
222
+ def update args = {}
223
+ @args.merge!(args)
224
+ save
225
+ end
226
+
227
+ def save
228
+ @args[:adgroup_id] = @adgroup.args[:adgroup_id] if !@args[:adgroup_id] && @adgroup.args[:adgroup_id]
229
+
109
230
  if @args[:keyword_id] #do update
110
-
231
+ #get current status of campaign
232
+ before_status = get_current_status
233
+
234
+ #restore campaign before update
235
+ restore if before_status == :stopped
236
+
237
+ begin
238
+ update_object
239
+
240
+ rescue Exception => e
241
+ log_error e.message
242
+ end
243
+
244
+ #remove it if new status is stopped or status doesn't changed and before it was stopped
245
+ remove if (@args[:status] == :stopped) || (@args[:status].nil? && before_status == :stopped)
111
246
  else #do save
112
- #create adtext
113
- create
247
+ #create keyword
248
+ begin
249
+ create
250
+
251
+ rescue Exception => e
252
+ log_error e.message
253
+
254
+ return false
255
+ end
256
+
257
+ #remove it if new status is stopped
258
+ remove if @args[:status] && @args[:status].to_s.to_sym == :stopped
114
259
  end
115
- end
260
+
261
+ !errors.any?
262
+ end
263
+
264
+ def log_error message
265
+ puts message
266
+ @adgroup.log_error "Keyword: #{@args[:keyword]} -> #{message}" if @adgroup
267
+ errors << message
268
+ end
269
+
116
270
  end
117
271
  end
118
-
272
+
@@ -4,15 +4,15 @@ class SklikApi
4
4
 
5
5
  NAME = "client"
6
6
 
7
- include Object
8
-
7
+ include SklikObject
8
+
9
9
  def initialize args = {}
10
10
  super args
11
11
  end
12
-
12
+
13
13
  def self.find args = {}
14
14
  out = connection.call("client.getAttributes") { |param|
15
- ([param[:user]]|param[:foreignAccounts]).collect{|u|
15
+ ([param[:user]]|param[:foreignAccounts]).collect{|u|
16
16
  u.symbolize_keys!
17
17
  SklikApi::Client.new(
18
18
  :customer_id => u[:userId],
@@ -1,21 +1,21 @@
1
1
  # -*- encoding : utf-8 -*-
2
2
  class SklikApi
3
3
  class Connection
4
-
4
+
5
5
  MAX_RETRIES = 3
6
6
  DEFAULTS = {
7
7
  :debug => false,
8
8
  :timeout => 100
9
9
  }
10
-
10
+
11
11
  def initialize args = {}
12
12
  @args = DEFAULTS.merge(args)
13
13
  end
14
-
14
+
15
15
  def self.connection
16
16
  @connection ||= SklikApi::Connection.new(:debug => false)
17
17
  end
18
-
18
+
19
19
  #prepare connection to sklik
20
20
  def connection
21
21
  path = (ENV['RACK_ENV'] || ENV['RAILS']) == "test" ? "/sandbox/RPC2" : "/RPC2"
@@ -24,11 +24,11 @@ class SklikApi
24
24
  #fix of UTF-8 encoding
25
25
  server.extend(XMLRPCWorkAround)
26
26
  #debug mode to see what XMLRPC is doing
27
- server.set_debug(File.open("log/xmlrpc-#{Time.now.strftime("%Y_%m_%d-%H_%M_%S")}.log","a:UTF-8")) if @args[:debug]
28
-
27
+ server.set_debug(File.open("log/xmlrpc-#{Time.now.strftime("%Y_%m_%d-%H_%M_%S")}.log","a:UTF-8")) if @args[:debug]
28
+
29
29
  server
30
30
  end
31
-
31
+
32
32
  #Get session is method for login into sklik
33
33
  #save session for other requests until it expires
34
34
  #every taxonomy has its own session!
@@ -38,7 +38,10 @@ class SklikApi
38
38
  @session[SklikApi::Access.uniq_identifier]
39
39
  else
40
40
  begin
41
+ SklikApi.log(:debug, "Getting session for #{SklikApi::Access.email}")
41
42
  param = connection.call("client.login", SklikApi::Access.email, SklikApi::Access.password).symbolize_keys
43
+ SklikApi.log(:debug, "Session received: #{param.inspect}")
44
+
42
45
  if param[:status] == 401
43
46
  raise ArgumentError, "Invalid login for: #{SklikApi::Access.email}"
44
47
  elsif param[:status] == 200
@@ -53,32 +56,52 @@ class SklikApi
53
56
  end
54
57
  end
55
58
  end
56
-
59
+
57
60
  # method to wrap method call to sklik -> allow retry and problem with session expiration
58
61
  def call method, *args
62
+
63
+ SklikApi.log(:debug, "Calling api: #{method} [#{args}]") unless method == "client.login"
59
64
  retry_count = MAX_RETRIES
60
- begin
65
+ begin
61
66
  #get response from sklik
62
67
  param = connection.call( method, get_session, *args ).symbolize_keys
63
- if [200,404].include?(param[:status])
68
+ SklikApi.log(:debug, "Response from api: #{param.inspect}") unless method == "client.login"
69
+ if [200].include?(param[:status])
64
70
  return yield(param)
71
+ elsif param[:status] == 400
72
+ raise SklikApi::InvalidArguments, "Calling method: #{method} with invalid arguments: #{args}, #{param.inspect}"
73
+ elsif param[:status] == 404
74
+ raise SklikApi::NotFound, "Calling method: #{method} with params: #{args} was not found"
65
75
  elsif param[:status] == 406
66
- raise ArgumentError, "Sklik returned: #{method}: #{param[:statusMessage]} #{args.inspect}"
76
+ raise SklikApi::InvalidData, print_errors(param[:errors])
67
77
  elsif param[:statusMessage] == "Session has expired or is malformed."
68
78
  raise ArgumentError, "session has expired"
69
79
  else
70
- raise ArgumentError, "There is error from sklik #{method}: #{param[:statusMessage]}: #{args.inspect}"
80
+ raise ArgumentError, "There is error from sklik #{method}: #{param.inspect}: #{args.inspect}"
71
81
  end
72
82
  rescue Exception => e
83
+ #when know exception which is not fault of sklik don't retry!
84
+ raise e if e.class.name =~ /SklikApi::/
85
+
73
86
  retry_count -= 1
74
87
  pp "Rescuing from request by: #{e.class} - #{e.message}"
75
88
  #if session expired then get new one! and retry
76
89
  get_session(true) if e.message == "session has expired"
77
90
  #don't retry if there is problem with Invalid paramaters od Data
78
- retry_count = 0 if e.message.include?("Invalid")
91
+ retry_count = 0 if e.message.include?("Invalid")
79
92
  retry if retry_count > 0
80
93
  raise e
81
94
  end
82
95
  end
96
+
97
+ def print_errors error_hash
98
+ error_hash.collect do |one_error|
99
+ out = "#{one_error.delete("id").humanize} ("
100
+ out += one_error.to_a.collect do |key, value|
101
+ "#{key} = #{value}"
102
+ end.join(", ")
103
+ out + ")"
104
+ end.join("; ")
105
+ end
83
106
  end
84
107
  end
@@ -0,0 +1,6 @@
1
+ class SklikApi
2
+
3
+ class NotFound < Exception; end
4
+ class InvalidArguments < Exception; end
5
+ class InvalidData < Exception; end
6
+ end