lardawge-rfm 1.3.1 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,388 @@
1
+ require 'net/https'
2
+ require 'cgi'
3
+ module Rfm
4
+ # This class represents a single FileMaker server. It is initialized with basic
5
+ # connection information, including the hostname, port number, and default database
6
+ # account name and password.
7
+ #
8
+ # Note: The host and port number refer to the FileMaker Web Publishing Engine, which
9
+ # must be installed and configured in order to use RFM. It may not actually be running
10
+ # on the same server computer as FileMaker Server itself. See your FileMaker Server
11
+ # or FileMaker Server Advanced documentation for information about configuring a Web
12
+ # Publishing Engine.
13
+ #
14
+ # =Accessing Databases
15
+ #
16
+ # Typically, you access a Database object from the Server like this:
17
+ #
18
+ # myDatabase = myServer["Customers"]
19
+ #
20
+ # This code gets the Database object representing the Customers object.
21
+ #
22
+ # Note: RFM does not talk to the server when you retrieve a database object in this way. Instead, it
23
+ # simply assumes you know what you're talking about. If the database you specify does not exist, you
24
+ # will get no error at this point. Instead, you'll get an error when you use the Layout object you get
25
+ # from this database. This makes debugging a little less convenient, but it would introduce too much
26
+ # overhead to hit the server at this point.
27
+ #
28
+ # The Server object has a +db+ attribute that provides alternate access to Database objects. It acts
29
+ # like a hash of Database objects, one for each accessible database on the server. So, for example, you
30
+ # can do this if you want to print out a list of all databses on the server:
31
+ #
32
+ # myServer.db.each {|database|
33
+ # puts database.name
34
+ # }
35
+ #
36
+ # The Server::db attribute is actually a DbFactory object, although it subclasses hash, so it should work
37
+ # in all the ways you expect. Note, though, that it is completely empty until the first time you attempt
38
+ # to access its elements. At that (lazy) point, it hits FileMaker, loads in the list of databases, and
39
+ # constructs a Database object for each one. In other words, it incurrs no overhead until you use it.
40
+ #
41
+ # =Attributes
42
+ #
43
+ # In addition to the +db+ attribute, Server has a few other useful attributes:
44
+ #
45
+ # * *host_name* is the host name this server points to
46
+ # * *port* is the port number this server communicates on
47
+ # * *state* is a hash of all server options used to initialize this server
48
+
49
+
50
+
51
+ # The Database object represents a single FileMaker Pro database. When you retrieve a Database
52
+ # object from a server, its account name and password are set to the account name and password you
53
+ # used when initializing the Server object. You can override this of course:
54
+ #
55
+ # myDatabase = myServer["Customers"]
56
+ # myDatabase.account_name = "foo"
57
+ # myDatabase.password = "bar"
58
+ #
59
+ # =Accessing Layouts
60
+ #
61
+ # All interaction with FileMaker happens through a Layout object. You can get a Layout object
62
+ # from the Database object like this:
63
+ #
64
+ # myLayout = myDatabase["Details"]
65
+ #
66
+ # This code gets the Layout object representing the layout called Details in the database.
67
+ #
68
+ # Note: RFM does not talk to the server when you retrieve a Layout object in this way. Instead, it
69
+ # simply assumes you know what you're talking about. If the layout you specify does not exist, you
70
+ # will get no error at this point. Instead, you'll get an error when you use the Layout object methods
71
+ # to talk to FileMaker. This makes debugging a little less convenient, but it would introduce too much
72
+ # overhead to hit the server at this point.
73
+ #
74
+ # The Database object has a +layout+ attribute that provides alternate access to Layout objects. It acts
75
+ # like a hash of Layout objects, one for each accessible layout in the database. So, for example, you
76
+ # can do this if you want to print out a list of all layouts:
77
+ #
78
+ # myDatabase.layout.each {|layout|
79
+ # puts layout.name
80
+ # }
81
+ #
82
+ # The Database::layout attribute is actually a LayoutFactory object, although it subclasses hash, so it
83
+ # should work in all the ways you expect. Note, though, that it is completely empty until the first time
84
+ # you attempt to access its elements. At that (lazy) point, it hits FileMaker, loads in the list of layouts,
85
+ # and constructs a Layout object for each one. In other words, it incurrs no overhead until you use it.
86
+ #
87
+ # =Accessing Scripts
88
+ #
89
+ # If for some reason you need to enumerate the scripts in a database, you can do so:
90
+ #
91
+ # myDatabase.script.each {|script|
92
+ # puts script.name
93
+ # }
94
+ #
95
+ # The Database::script attribute is actually a ScriptFactory object, although it subclasses hash, so it
96
+ # should work in all the ways you expect. Note, though, that it is completely empty until the first time
97
+ # you attempt to access its elements. At that (lazy) point, it hits FileMaker, loads in the list of scripts,
98
+ # and constructs a Script object for each one. In other words, it incurrs no overhead until you use it.
99
+ #
100
+ # Note: You don't need a Script object to _run_ a script (see the Layout object instead).
101
+ #
102
+ # =Attributes
103
+ #
104
+ # In addition to the +layout+ attribute, Server has a few other useful attributes:
105
+ #
106
+ # * *server* is the Server object this database comes from
107
+ # * *name* is the name of this database
108
+ # * *state* is a hash of all server options used to initialize this server
109
+ class Server
110
+ #
111
+ # To create a Server object, you typically need at least a host name:
112
+ #
113
+ # myServer = Rfm::Server.new({:host => 'my.host.com'})
114
+ #
115
+ # Several other options are supported:
116
+ #
117
+ # * *host* the hostname of the Web Publishing Engine (WPE) server (defaults to 'localhost')
118
+ #
119
+ # * *port* the port number the WPE is listening no (defaults to 80 unless *ssl* +true+ which sets it to 443)
120
+ #
121
+ # * *account_name* the default account name to log in to databases with (you can also supply a
122
+ # account name on a per-database basis if necessary)
123
+ #
124
+ # * *password* the default password to log in to databases with (you can also supplly a password
125
+ # on a per-databases basis if necessary)
126
+ #
127
+ # * *log_actions* when +true+, RFM logs all action URLs that are sent to FileMaker server to stderr
128
+ # (defaults to +false+)
129
+ #
130
+ # * *log_responses* when +true+, RFM logs all raw XML responses (including headers) from FileMaker to
131
+ # stderr (defaults to +false+)
132
+ #
133
+ # * *warn_on_redirect* normally, RFM prints a warning to stderr if the Web Publishing Engine redirects
134
+ # (this can usually be fixed by using a different host name, which speeds things up); if you *don't*
135
+ # want this warning printed, set +warn_on_redirect+ to +true+
136
+ #
137
+ # * *raise_on_401* although RFM raises error when FileMaker returns error responses, it typically
138
+ # ignores FileMaker's 401 error (no records found) and returns an empty record set instead; if you
139
+ # prefer a raised error when a find produces no errors, set this option to +true+
140
+ #
141
+ #SSL Options (SSL AND CERTIFICATE VERIFICATION ARE ON BY DEFAULT):
142
+ #
143
+ # * *ssl* +false+ if you want to turn SSL (HTTPS) off when connecting to connect to FileMaker (default is +true+)
144
+ #
145
+ # If you are using SSL and want to verify the certificate use the following options:
146
+ #
147
+ # * *root_cert* +false+ if you do not want to verify your SSL session (default is +true+).
148
+ # You will want to turn this off if you are using a self signed certificate and do not have a certificate authority cert file.
149
+ # If you choose this option you will need to provide a cert *root_cert_name* and *root_cert_path* (if not in root directory).
150
+ #
151
+ # * *root_cert_name* name of pem file for certificate verification (Root cert from certificate authority who issued certificate.
152
+ # If self signed certificate do not use this option!!). You can download the entire bundle of CA Root Certificates
153
+ # from http://curl.haxx.se/ca/cacert.pem. Place the pem file in config directory.
154
+ #
155
+ # * *root_cert_path* path to cert file. (defaults to '/' if no path given)
156
+ #
157
+ #Configuration Examples:
158
+ #
159
+ # Example to turn off SSL:
160
+ #
161
+ # myServer = Rfm::Server.new({
162
+ # :host => 'localhost',
163
+ # :account_name => 'sample',
164
+ # :password => '12345',
165
+ # :ssl => false
166
+ # })
167
+ #
168
+ # Example using SSL without *root_cert*:
169
+ #
170
+ # myServer = Rfm::Server.new({
171
+ # :host => 'localhost',
172
+ # :account_name => 'sample',
173
+ # :password => '12345',
174
+ # :root_cert => false
175
+ # })
176
+ #
177
+ # Example using SSL with *root_cert* at file root:
178
+ #
179
+ # myServer = Rfm::Server.new({
180
+ # :host => 'localhost',
181
+ # :account_name => 'sample',
182
+ # :password => '12345',
183
+ # :root_cert_name => 'example.pem'
184
+ # })
185
+ #
186
+ # Example using SSL with *root_cert* specifying *root_cert_path*:
187
+ #
188
+ # myServer = Rfm::Server.new({
189
+ # :host => 'localhost',
190
+ # :account_name => 'sample',
191
+ # :password => '12345',
192
+ # :root_cert_name => 'example.pem'
193
+ # :root_cert_path => '/usr/cert_file/'
194
+ # })
195
+
196
+ def initialize(options)
197
+ @state = {
198
+ :host => 'localhost',
199
+ :port => 80,
200
+ :ssl => true,
201
+ :root_cert => true,
202
+ :root_cert_name => '',
203
+ :root_cert_path => '/',
204
+ :account_name => '',
205
+ :password => '',
206
+ :log_actions => false,
207
+ :log_responses => false,
208
+ :warn_on_redirect => true,
209
+ :raise_on_401 => false
210
+ }.merge(options)
211
+
212
+ @state.freeze
213
+
214
+ @host_name = @state[:host]
215
+ @scheme = @state[:ssl] ? "https" : "http"
216
+ @port = @state[:ssl] && options[:port].nil? ? 443 : @state[:port]
217
+
218
+ @db = Rfm::Factory::DbFactory.new(self)
219
+ end
220
+
221
+ # Access the database object representing a database on the server. For example:
222
+ #
223
+ # myServer['Customers']
224
+ #
225
+ # would return a Database object representing the _Customers_
226
+ # database on the server.
227
+ #
228
+ # Note: RFM never talks to the server until you perform an action. The database object
229
+ # returned is created on the fly and assumed to refer to a valid database, but you will
230
+ # get no error at this point if the database you access doesn't exist. Instead, you'll
231
+ # receive an error when you actually try to perform some action on a layout from this
232
+ # database.
233
+ def [](dbname)
234
+ self.db[dbname]
235
+ end
236
+
237
+ attr_reader :db, :host_name, :port, :scheme, :state
238
+
239
+ # Performs a raw FileMaker action. You will generally not call this method directly, but it
240
+ # is exposed in case you need to do something "under the hood."
241
+ #
242
+ # The +action+ parameter is any valid FileMaker web url action. For example, +-find+, +-finadny+ etc.
243
+ #
244
+ # The +args+ parameter is a hash of arguments to be included in the action url. It will be serialized
245
+ # and url-encoded appropriately.
246
+ #
247
+ # The +options+ parameter is a hash of RFM-specific options, which correspond to the more esoteric
248
+ # FileMaker URL parameters. They are exposed separately because they can also be passed into
249
+ # various methods on the Layout object, which is a much more typical way of sending an action to
250
+ # FileMaker.
251
+ #
252
+ # This method returns the Net::HTTP response object representing the response from FileMaker.
253
+ #
254
+ # For example, if you wanted to send a raw command to FileMaker to find the first 20 people in the
255
+ # "Customers" database whose first name is "Bill" you might do this:
256
+ #
257
+ # response = myServer.do_action(
258
+ # '-find',
259
+ # {
260
+ # "-db" => "Customers",
261
+ # "-lay" => "Details",
262
+ # "First Name" => "Bill"
263
+ # },
264
+ # { :max_records => 20 }
265
+ # )
266
+ def do_action(account_name, password, action, args, options = {})
267
+ post = args.merge(expand_options(options)).merge({action => ''})
268
+ http_fetch(@host_name, @port, "/fmi/xml/fmresultset.xml", account_name, password, post)
269
+ end
270
+
271
+ def load_layout(layout)
272
+ post = {'-db' => layout.db.name, '-lay' => layout.name, '-view' => ''}
273
+ http_fetch(@host_name, @port, "/fmi/xml/FMPXMLLAYOUT.xml", layout.db.account_name, layout.db.password, post)
274
+ end
275
+
276
+ private
277
+
278
+ def http_fetch(host_name, port, path, account_name, password, post_data, limit=10)
279
+ raise Rfm::Error::CommunicationError.new("While trying to reach the Web Publishing Engine, RFM was redirected too many times.") if limit == 0
280
+
281
+ if @state[:log_actions] == true
282
+ qs = post_data.collect{|key,val| "#{CGI::escape(key.to_s)}=#{CGI::escape(val.to_s)}"}.join("&")
283
+ warn "#{@scheme}://#{@host_name}:#{@port}#{path}?#{qs}"
284
+ end
285
+
286
+ request = Net::HTTP::Post.new(path)
287
+ request.basic_auth(account_name, password)
288
+ request.set_form_data(post_data)
289
+
290
+ response = Net::HTTP.new(host_name, port)
291
+
292
+ if @state[:ssl]
293
+ response.use_ssl = true
294
+ if @state[:root_cert]
295
+ response.verify_mode = OpenSSL::SSL::VERIFY_PEER
296
+ response.ca_file = File.join(@state[:root_cert_path], @state[:root_cert_name])
297
+ else
298
+ response.verify_mode = OpenSSL::SSL::VERIFY_NONE
299
+ end
300
+ end
301
+
302
+ response = response.start { |http| http.request(request) }
303
+
304
+ if @state[:log_responses] == true
305
+ response.to_hash.each { |key, value| warn "#{key}: #{value}" }
306
+ warn response.body
307
+ end
308
+
309
+ case response
310
+ when Net::HTTPSuccess
311
+ response
312
+ when Net::HTTPRedirection
313
+ if @state[:warn_on_redirect]
314
+ warn "The web server redirected to " + response['location'] +
315
+ ". You should revise your connection hostname or fix your server configuration if possible to improve performance."
316
+ end
317
+ newloc = URI.parse(response['location'])
318
+ http_fetch(newloc.host, newloc.port, newloc.request_uri, account_name, password, post_data, limit - 1)
319
+ when Net::HTTPUnauthorized
320
+ msg = "The account name (#{account_name}) or password provided is not correct (or the account doesn't have the fmxml extended privilege)."
321
+ raise Rfm::Error::AuthenticationError.new(msg)
322
+ when Net::HTTPNotFound
323
+ msg = "Could not talk to FileMaker because the Web Publishing Engine is not responding (server returned 404)."
324
+ raise Rfm::Error::CommunicationError.new(msg)
325
+ else
326
+ msg = "Unexpected response from server: #{result.code} (#{result.class.to_s}). Unable to communicate with the Web Publishing Engine."
327
+ raise Rfm::Error::CommunicationError.new(msg)
328
+ end
329
+ end
330
+
331
+ def expand_options(options)
332
+ result = {}
333
+ options.each do |key,value|
334
+ case key
335
+ when :max_records
336
+ result['-max'] = value
337
+ when :skip_records
338
+ result['-skip'] = value
339
+ when :sort_field
340
+ if value.kind_of? Array
341
+ raise Rfm::Error::ParameterError.new(":sort_field can have at most 9 fields, but you passed an array with #{value.size} elements.") if value.size > 9
342
+ value.each_index { |i| result["-sortfield.#{i+1}"] = value[i] }
343
+ else
344
+ result["-sortfield.1"] = value
345
+ end
346
+ when :sort_order
347
+ if value.kind_of? Array
348
+ raise Rfm::Error::ParameterError.new(":sort_order can have at most 9 fields, but you passed an array with #{value.size} elements.") if value.size > 9
349
+ value.each_index { |i| result["-sortorder.#{i+1}"] = value[i] }
350
+ else
351
+ result["-sortorder.1"] = value
352
+ end
353
+ when :post_script
354
+ if value.class == Array
355
+ result['-script'] = value[0]
356
+ result['-script.param'] = value[1]
357
+ else
358
+ result['-script'] = value
359
+ end
360
+ when :pre_find_script
361
+ if value.class == Array
362
+ result['-script.prefind'] = value[0]
363
+ result['-script.prefind.param'] = value[1]
364
+ else
365
+ result['-script.presort'] = value
366
+ end
367
+ when :pre_sort_script
368
+ if value.class == Array
369
+ result['-script.presort'] = value[0]
370
+ result['-script.presort.param'] = value[1]
371
+ else
372
+ result['-script.presort'] = value
373
+ end
374
+ when :response_layout
375
+ result['-lay.response'] = value
376
+ when :logical_operator
377
+ result['-lop'] = value
378
+ when :modification_id
379
+ result['-modid'] = value
380
+ else
381
+ raise Rfm::Error::ParameterError.new("Invalid option: #{key} (are you using a string instead of a symbol?)")
382
+ end
383
+ end
384
+ return result
385
+ end
386
+
387
+ end
388
+ end
data/lib/rfm/error.rb ADDED
@@ -0,0 +1,256 @@
1
+ require "set"
2
+
3
+ # These classes wrap the filemaker error codes. FileMakerError is the base class of this hierarchy.
4
+ #
5
+ # One could get a FileMakerError by doing:
6
+ # err = Rfm::Error::FileMakerError.getError(102)
7
+ #
8
+ # The above code would return a FieldMissingError instance. Your could use this instance to raise that appropriate
9
+ # exception:
10
+ #
11
+ # raise err
12
+ #
13
+ # You could access the specific error code by accessing:
14
+ #
15
+ # err.code
16
+ #
17
+ # Author:: Mufaddal Khumri
18
+ # Copyright:: Copyright (c) 2007 Six Fried Rice, LLC and Mufaddal Khumri
19
+ # License:: See MIT-LICENSE for details
20
+ module Rfm
21
+ module Error
22
+
23
+ class RfmError < StandardError
24
+ end
25
+
26
+ class CommunicationError < RfmError
27
+ end
28
+
29
+ class ParameterError < RfmError
30
+ end
31
+
32
+ class AuthenticationError < RfmError
33
+ end
34
+
35
+ # Base class for all FileMaker errors
36
+ class FileMakerError < RfmError
37
+ attr_accessor :code
38
+
39
+ # Default filemaker error message map
40
+ @default_messages = {}
41
+ class << self; attr_reader :default_messages; end
42
+
43
+
44
+ # This method instantiates and returns the appropriate FileMakerError object depending on the error code passed to it. It
45
+ # also accepts an optional message.
46
+ def self.getError(code, message = nil)
47
+ if @default_messages == nil or @default_messages.size == 0
48
+ (0..99).each{|i| @default_messages[i] = 'SystemError occurred.'}
49
+ (100..199).each{|i| @default_messages[i] = 'MissingError occurred.'}
50
+ @default_messages[102] = 'FieldMissingError occurred.'
51
+ @default_messages[104] = 'ScriptMissingError occurred.'
52
+ @default_messages[105] = 'LayoutMissingError occurred.'
53
+ @default_messages[106] = 'TableMissingError occurred.'
54
+ (200..299).each{|i| @default_messages[i] = 'SecurityError occurred.'}
55
+ @default_messages[200] = 'RecordAccessDeniedError occurred.'
56
+ @default_messages[201] = 'FieldCannotBeModifiedError occurred.'
57
+ @default_messages[202] = 'FieldAccessIsDeniedError occurred.'
58
+ (300..399).each{|i| @default_messages[i] = 'ConcurrencyError occurred.'}
59
+ @default_messages[301] = 'RecordInUseError occurred.'
60
+ @default_messages[302] = 'TableInUseError occurred.'
61
+ @default_messages[306] = 'RecordModIdDoesNotMatchError occurred.'
62
+ (400..499).each{|i| @default_messages[i] = 'GeneralError occurred.'}
63
+ @default_messages[401] = 'NoRecordsFoundError occurred.'
64
+ (500..599).each{|i| @default_messages[i] = 'ValidationError occurred.'}
65
+ @default_messages[500] = 'DateValidationError occurred.'
66
+ @default_messages[501] = 'TimeValidationError occurred.'
67
+ @default_messages[502] = 'NumberValidationError occurred.'
68
+ @default_messages[503] = 'RangeValidationError occurred.'
69
+ @default_messages[504] = 'UniqueValidationError occurred.'
70
+ @default_messages[505] = 'ExistingValidationError occurred.'
71
+ @default_messages[506] = 'ValueListValidationError occurred.'
72
+ @default_messages[507] = 'ValidationCalculationError occurred.'
73
+ @default_messages[508] = 'InvalidFindModeValueError occurred.'
74
+ @default_messages[511] = 'MaximumCharactersValidationError occurred.'
75
+ (800..899).each{|i| @default_messages[i] = 'FileError occurred.'}
76
+ @default_messages[802] = 'UnableToOpenFileError occurred.'
77
+ end
78
+
79
+ message = @default_messages[code] if message == nil || message.strip == ''
80
+ message += " (FileMaker Error ##{code})"
81
+
82
+ if 0 <= code and code <= 99
83
+ err = SystemError.new(message)
84
+ elsif 100 <= code and code <= 199
85
+ if code == 101
86
+ err = RecordMissingError.new(message)
87
+ elsif code == 102
88
+ err = FieldMissingError.new(message)
89
+ elsif code == 104
90
+ err = ScriptMissingError.new(message)
91
+ elsif code == 105
92
+ err = LayoutMissingError.new(message)
93
+ elsif code == 106
94
+ err = TableMissingError.new(message)
95
+ else
96
+ err = MissingError.new(message)
97
+ end
98
+ elsif 200 <= code and code <= 299
99
+ if code == 200
100
+ err = RecordAccessDeniedError.new(message)
101
+ elsif code == 201
102
+ err = FieldCannotBeModifiedError.new(message)
103
+ elsif code == 202
104
+ err = FieldAccessIsDeniedError.new(message)
105
+ else
106
+ err = SecurityError.new(message)
107
+ end
108
+ elsif 300 <= code and code <= 399
109
+ if code == 301
110
+ err = RecordInUseError.new(message)
111
+ elsif code == 302
112
+ err = TableInUseError.new(message)
113
+ elsif code == 306
114
+ err = RecordModIdDoesNotMatchError.new(message)
115
+ else
116
+ err = ConcurrencyError.new(message)
117
+ end
118
+ elsif 400 <= code and code <= 499
119
+ if code == 401
120
+ err = NoRecordsFoundError.new(message)
121
+ else
122
+ err = GeneralError.new(message)
123
+ end
124
+ elsif 500 <= code and code <= 599
125
+ if code == 500
126
+ err = DateValidationError.new(message)
127
+ elsif code == 501
128
+ err = TimeValidationError.new(message)
129
+ elsif code == 502
130
+ err = NumberValidationError.new(message)
131
+ elsif code == 503
132
+ err = RangeValidationError.new(message)
133
+ elsif code == 504
134
+ err = UniqueValidationError.new(message)
135
+ elsif code == 505
136
+ err = ExistingValidationError.new(message)
137
+ elsif code == 506
138
+ err = ValueListValidationError.new(message)
139
+ elsif code == 507
140
+ err = ValidationCalculationError.new(message)
141
+ elsif code == 508
142
+ err = InvalidFindModeValueError.new(message)
143
+ elsif code == 511
144
+ err = MaximumCharactersValidationError.new(message)
145
+ else
146
+ err = ValidationError.new(message)
147
+ end
148
+ elsif 800 <= code and code <= 899
149
+ if code == 802
150
+ err = UnableToOpenFileError.new(message)
151
+ else
152
+ err = FileError.new(message)
153
+ end
154
+ else
155
+ # called for code == -1 or any other code not handled above.
156
+ err = UnknownError.new(message)
157
+ end
158
+ err.code = code
159
+ return err
160
+ end
161
+ end
162
+
163
+ class UnknownError < FileMakerError
164
+ end
165
+
166
+ class SystemError < FileMakerError
167
+ end
168
+
169
+ class MissingError < FileMakerError
170
+ end
171
+
172
+ class RecordMissingError < MissingError
173
+ end
174
+
175
+ class FieldMissingError < MissingError
176
+ end
177
+
178
+ class ScriptMissingError < MissingError
179
+ end
180
+
181
+ class LayoutMissingError < MissingError
182
+ end
183
+
184
+ class TableMissingError < MissingError
185
+ end
186
+
187
+ class SecurityError < FileMakerError
188
+ end
189
+
190
+ class RecordAccessDeniedError < SecurityError
191
+ end
192
+
193
+ class FieldCannotBeModifiedError < SecurityError
194
+ end
195
+
196
+ class FieldAccessIsDeniedError < SecurityError
197
+ end
198
+
199
+ class ConcurrencyError < FileMakerError
200
+ end
201
+
202
+ class RecordInUseError < ConcurrencyError
203
+ end
204
+
205
+ class TableInUseError < ConcurrencyError
206
+ end
207
+
208
+ class RecordModIdDoesNotMatchError < ConcurrencyError
209
+ end
210
+
211
+ class GeneralError < FileMakerError
212
+ end
213
+
214
+ class NoRecordsFoundError < GeneralError
215
+ end
216
+
217
+ class ValidationError < FileMakerError
218
+ end
219
+
220
+ class DateValidationError < ValidationError
221
+ end
222
+
223
+ class TimeValidationError < ValidationError
224
+ end
225
+
226
+ class NumberValidationError < ValidationError
227
+ end
228
+
229
+ class RangeValidationError < ValidationError
230
+ end
231
+
232
+ class UniqueValidationError < ValidationError
233
+ end
234
+
235
+ class ExistingValidationError < ValidationError
236
+ end
237
+
238
+ class ValueListValidationError < ValidationError
239
+ end
240
+
241
+ class ValidationCalculationError < ValidationError
242
+ end
243
+
244
+ class InvalidFindModeValueError < ValidationError
245
+ end
246
+
247
+ class MaximumCharactersValidationError < ValidationError
248
+ end
249
+
250
+ class FileError < FileMakerError
251
+ end
252
+
253
+ class UnableToOpenFileError < FileError
254
+ end
255
+ end
256
+ end