lardawge-rfm 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.
@@ -0,0 +1,713 @@
1
+ require 'net/http'
2
+ require 'rexml/document'
3
+ require 'cgi'
4
+
5
+ # This module includes classes that represent base FileMaker concepts like servers,
6
+ # layouts, and scripts. These classes allow you to communicate with FileMaker Server,
7
+ # send commands, and receive responses.
8
+ #
9
+ # Author:: Geoff Coffey (mailto:gwcoffey@gmail.com)
10
+ # Copyright:: Copyright (c) 2007 Six Fried Rice, LLC and Mufaddal Khumri
11
+ # License:: See MIT-LICENSE for details
12
+ module Rfm
13
+
14
+ # This class represents a single FileMaker server. It is initialized with basic
15
+ # connection information, including the hostname, port number, and default database
16
+ # account name and password.
17
+ #
18
+ # Note: The host and port number refer to the FileMaker Web Publishing Engine, which
19
+ # must be installed and configured in order to use RFM. It may not actually be running
20
+ # on the same server computer as FileMaker Server itself. See your FileMaker Server
21
+ # or FileMaker Server Advanced documentation for information about configuring a Web
22
+ # Publishing Engine.
23
+ #
24
+ # =Accessing Databases
25
+ #
26
+ # Typically, you access a Database object from the Server like this:
27
+ #
28
+ # myDatabase = myServer["Customers"]
29
+ #
30
+ # This code gets the Database object representing the Customers object.
31
+ #
32
+ # Note: RFM does not talk to the server when you retrieve a database object in this way. Instead, it
33
+ # simply assumes you know what you're talking about. If the database you specify does not exist, you
34
+ # will get no error at this point. Instead, you'll get an error when you use the Layout object you get
35
+ # from this database. This makes debugging a little less convenient, but it would introduce too much
36
+ # overhead to hit the server at this point.
37
+ #
38
+ # The Server object has a +db+ attribute that provides alternate access to Database objects. It acts
39
+ # like a hash of Database objects, one for each accessible database on the server. So, for example, you
40
+ # can do this if you want to print out a list of all databses on the server:
41
+ #
42
+ # myServer.db.each {|database|
43
+ # puts database.name
44
+ # }
45
+ #
46
+ # The Server::db attribute is actually a DbFactory object, although it subclasses hash, so it should work
47
+ # in all the ways you expect. Note, though, that it is completely empty until the first time you attempt
48
+ # to access its elements. At that (lazy) point, it hits FileMaker, loads in the list of databases, and
49
+ # constructs a Database object for each one. In other words, it incurrs no overhead until you use it.
50
+ #
51
+ # =Attributes
52
+ #
53
+ # In addition to the +db+ attribute, Server has a few other useful attributes:
54
+ #
55
+ # * *host_name* is the host name this server points to
56
+ # * *post* is the port number this server communicates on
57
+ # * *state* is a hash of all server options used to initialize this server
58
+
59
+ class Server
60
+
61
+ # To create a Server obejct, you typically need at least a host name:
62
+ #
63
+ # myServer = Rfm::Server.new({:host => 'my.host.com'})
64
+ #
65
+ # Several other options are supported:
66
+ #
67
+ # * *host* the hostname of the Web Publishing Engine (WPE) server (defaults to 'localhost')
68
+ #
69
+ # * *port* the port number the WPE is listening no (defaults to 80)
70
+ #
71
+ # * *ssl* +true+ if you want to use SSL (HTTPS) to connect to FileMaker (defaults to +false+)
72
+ #
73
+ # * *account_name* the default account name to log in to databases with (you can also supply a
74
+ # account name on a per-database basis if necessary)
75
+ #
76
+ # * *password* the default password to log in to databases with (you can also supplly a password
77
+ # on a per-databases basis if necessary)
78
+ #
79
+ # * *log_actions* when +true+, RFM logs all action URLs that are sent to FileMaker server to stderr
80
+ # (defaults to +false+)
81
+ #
82
+ # * *log_responses* when +true+, RFM logs all raw XML responses (including headers) from FileMaker to
83
+ # stderr (defaults to +false+)
84
+ #
85
+ # * *warn_on_redirect* normally, RFM prints a warning to stderr if the Web Publishing Engine redirects
86
+ # (this can usually be fixed by using a different host name, which speeds things up); if you *don't*
87
+ # want this warning printed, set +warn_on_redirect+ to +true+
88
+ #
89
+ # * *raise_on_401* although RFM raises error when FileMaker returns error responses, it typically
90
+ # ignores FileMaker's 401 error (no records found) and returns an empty record set instead; if you
91
+ # prefer a raised error when a find produces no errors, set this option to +true+
92
+ def initialize(options)
93
+ @state = {
94
+ :host => 'localhost',
95
+ :port => 80,
96
+ :ssl => false,
97
+ :account_name => '',
98
+ :password => '',
99
+ :log_actions => false,
100
+ :log_responses => false,
101
+ :warn_on_redirect => true,
102
+ :raise_on_401 => false
103
+ }.merge(options)
104
+
105
+ if @state[:username] != nil
106
+ warn("the :username option on Rfm::Server::initialize has been deprecated. Use :account_name instead.")
107
+ @state[:account_name] = @state[:username]
108
+ end
109
+
110
+ @state.freeze
111
+
112
+ @host_name = @state[:host]
113
+ @scheme = @state[:ssl] ? "https" : "http"
114
+ @port = @state[:port]
115
+
116
+ @db = Rfm::Factory::DbFactory.new(self)
117
+ end
118
+
119
+ # Access the database object representing a database on the server. For example:
120
+ #
121
+ # myServer['Customers']
122
+ #
123
+ # would return a Database object representing the _Customers_
124
+ # database on the server.
125
+ #
126
+ # Note: RFM never talks to the server until you perform an action. The database object
127
+ # returned is created on the fly and assumed to refer to a valid database, but you will
128
+ # get no error at this point if the database you access doesn't exist. Instead, you'll
129
+ # receive an error when you actually try to perform some action on a layout from this
130
+ # database.
131
+ def [](dbname)
132
+ self.db[dbname]
133
+ end
134
+
135
+ attr_reader :db, :host_name, :port, :scheme, :state
136
+
137
+ # Performs a raw FileMaker action. You will generally not call this method directly, but it
138
+ # is exposed in case you need to do something "under the hood."
139
+ #
140
+ # The +action+ parameter is any valid FileMaker web url action. For example, +-find+, +-finadny+ etc.
141
+ #
142
+ # The +args+ parameter is a hash of arguments to be included in the action url. It will be serialized
143
+ # and url-encoded appropriately.
144
+ #
145
+ # The +options+ parameter is a hash of RFM-specific options, which correspond to the more esoteric
146
+ # FileMaker URL parameters. They are exposed separately because they can also be passed into
147
+ # various methods on the Layout object, which is a much more typical way of sending an action to
148
+ # FileMaker.
149
+ #
150
+ # This method returns the Net::HTTP response object representing the response from FileMaker.
151
+ #
152
+ # For example, if you wanted to send a raw command to FileMaker to find the first 20 people in the
153
+ # "Customers" database whose first name is "Bill" you might do this:
154
+ #
155
+ # response = myServer.do_action(
156
+ # '-find',
157
+ # {
158
+ # "-db" => "Customers",
159
+ # "-lay" => "Details",
160
+ # "First Name" => "Bill"
161
+ # },
162
+ # { :max_records => 20 }
163
+ # )
164
+ def do_action(account_name, password, action, args, options = {})
165
+ post = args.merge(expand_options(options)).merge({action => ''})
166
+ http_fetch(@host_name, @port, "/fmi/xml/fmresultset.xml", account_name, password, post)
167
+ end
168
+
169
+ def load_layout(layout)
170
+ post = {'-db' => layout.db.name, '-lay' => layout.name, '-view' => ''}
171
+ http_fetch(@host_name, @port, "/fmi/xml/FMPXMLLAYOUT.xml", layout.db.account_name, layout.db.password, post)
172
+ end
173
+
174
+ private
175
+
176
+ def http_fetch(host_name, port, path, account_name, password, post_data, limit = 10)
177
+ if limit == 0
178
+ raise Rfm::Error::CommunicationError.new("While trying to reach the Web Publishing Engine, RFM was redirected too many times.")
179
+ end
180
+
181
+ if @state[:log_actions] == true
182
+ qs = post_data.collect{|key,val| "#{CGI::escape(key.to_s)}=#{CGI::escape(val.to_s)}"}.join("&")
183
+ warn "#{@scheme}://#{@host_name}:#{@port}#{path}?#{qs}"
184
+ end
185
+
186
+ request = Net::HTTP::Post.new(path)
187
+ request.basic_auth(account_name, password)
188
+ request.set_form_data(post_data)
189
+
190
+ response = Net::HTTP.start(host_name, port) { |http|
191
+ http.request(request)
192
+ }
193
+
194
+ if @state[:log_responses] == true
195
+ response.to_hash.each {|key, value|
196
+ warn "#{key}: #{value}"
197
+ }
198
+ warn response.body
199
+ end
200
+
201
+ case response
202
+ when Net::HTTPSuccess
203
+ response
204
+ when Net::HTTPRedirection
205
+ if @state[:warn_on_redirect]
206
+ warn "The web server redirected to " + response['location'] + ". You should revise your connection hostname or fix your server configuration if possible to improve performance."
207
+ end
208
+ newloc = URI.parse(response['location'])
209
+ http_fetch(newloc.host, newloc.port, newloc.request_uri, account_name, password, post_data, limit - 1)
210
+ when Net::HTTPUnauthorized
211
+ msg = "The account name (#{account_name}) or password provided is not correct (or the account doesn't have the fmxml extended privilege)."
212
+ raise Rfm::Error::AuthenticationError.new(msg)
213
+ when Net::HTTPNotFound
214
+ msg = "Could not talk to FileMaker because the Web Publishing Engine is not responding (server returned 404)."
215
+ raise Rfm::Error::CommunicationError.new(msg)
216
+ else
217
+ msg = "Unexpected response from server: #{result.code} (#{result.class.to_s}). Unable to communicate with the Web Publishing Engine."
218
+ raise Rfm::Error::CommunicationError.new(msg)
219
+ end
220
+ end
221
+
222
+ def expand_options(options)
223
+ result = {}
224
+ options.each {|key,value|
225
+ case key
226
+ when :max_records:
227
+ result['-max'] = value
228
+ when :skip_records:
229
+ result['-skip'] = value
230
+ when :sort_field:
231
+ if value.kind_of? Array
232
+ if value.size > 9
233
+ raise Rfm::Error::ParameterError.new(":sort_field can have at most 9 fields, but you passed an array with #{value.size} elements.")
234
+ end
235
+ value.each_index {|i|
236
+ result["-sortfield.#{i+1}"] = value[i]
237
+ }
238
+ else
239
+ result["-sortfield.1"] = value
240
+ end
241
+ when :sort_order:
242
+ result['-sortorder'] = value
243
+ when :post_script:
244
+ if value.class == Array
245
+ result['-script'] = value[0]
246
+ result['-script.param'] = value[1]
247
+ else
248
+ result['-script'] = value
249
+ end
250
+ when :pre_find_script:
251
+ if value.class == Array
252
+ result['-script.prefind'] = value[0]
253
+ result['-script.prefind.param'] = value[1]
254
+ else
255
+ result['-script.presort'] = value
256
+ end
257
+ when :pre_sort_script:
258
+ if value.class == Array
259
+ result['-script.presort'] = value[0]
260
+ result['-script.presort.param'] = value[1]
261
+ else
262
+ result['-script.presort'] = value
263
+ end
264
+ when :response_layout:
265
+ result['-lay.response'] = value
266
+ when :logical_operator:
267
+ result['-lop'] = value
268
+ when :modification_id:
269
+ result['-modid'] = value
270
+ else
271
+ raise Rfm::Error::ParameterError.new("Invalid option: #{key} (are you using a string instead of a symbol?)")
272
+ end
273
+ }
274
+ result
275
+ end
276
+
277
+ end
278
+
279
+ # The Database object represents a single FileMaker Pro database. When you retrieve a Database
280
+ # object from a server, its account name and password are set to the account name and password you
281
+ # used when initializing the Server object. You can override this of course:
282
+ #
283
+ # myDatabase = myServer["Customers"]
284
+ # myDatabase.account_name = "foo"
285
+ # myDatabase.password = "bar"
286
+ #
287
+ # =Accessing Layouts
288
+ #
289
+ # All interaction with FileMaker happens through a Layout object. You can get a Layout object
290
+ # from the Database object like this:
291
+ #
292
+ # myLayout = myDatabase["Details"]
293
+ #
294
+ # This code gets the Layout object representing the layout called Details in the database.
295
+ #
296
+ # Note: RFM does not talk to the server when you retrieve a Layout object in this way. Instead, it
297
+ # simply assumes you know what you're talking about. If the layout you specify does not exist, you
298
+ # will get no error at this point. Instead, you'll get an error when you use the Layout object methods
299
+ # to talk to FileMaker. This makes debugging a little less convenient, but it would introduce too much
300
+ # overhead to hit the server at this point.
301
+ #
302
+ # The Database object has a +layout+ attribute that provides alternate access to Layout objects. It acts
303
+ # like a hash of Layout objects, one for each accessible layout in the database. So, for example, you
304
+ # can do this if you want to print out a list of all layouts:
305
+ #
306
+ # myDatabase.layout.each {|layout|
307
+ # puts layout.name
308
+ # }
309
+ #
310
+ # The Database::layout attribute is actually a LayoutFactory object, although it subclasses hash, so it
311
+ # should work in all the ways you expect. Note, though, that it is completely empty until the first time
312
+ # you attempt to access its elements. At that (lazy) point, it hits FileMaker, loads in the list of layouts,
313
+ # and constructs a Layout object for each one. In other words, it incurrs no overhead until you use it.
314
+ #
315
+ # =Accessing Scripts
316
+ #
317
+ # If for some reason you need to enumerate the scripts in a database, you can do so:
318
+ #
319
+ # myDatabase.script.each {|script|
320
+ # puts script.name
321
+ # }
322
+ #
323
+ # The Database::script attribute is actually a ScriptFactory object, although it subclasses hash, so it
324
+ # should work in all the ways you expect. Note, though, that it is completely empty until the first time
325
+ # you attempt to access its elements. At that (lazy) point, it hits FileMaker, loads in the list of scripts,
326
+ # and constructs a Script object for each one. In other words, it incurrs no overhead until you use it.
327
+ #
328
+ # Note: You don't need a Script object to _run_ a script (see the Layout object instead).
329
+ #
330
+ # =Attributes
331
+ #
332
+ # In addition to the +layout+ attribute, Server has a few other useful attributes:
333
+ #
334
+ # * *server* is the Server object this database comes from
335
+ # * *name* is the name of this database
336
+ # * *state* is a hash of all server options used to initialize this server
337
+ class Database
338
+
339
+ # Initialize a database object. You never really need to do this. Instead, just do this:
340
+ #
341
+ # myServer = Rfm::Server.new(...)
342
+ # myDatabase = myServer["Customers"]
343
+ #
344
+ # This sample code gets a database object representing the Customers database on the FileMaker server.
345
+ def initialize(name, server)
346
+ @name = name
347
+ @server = server
348
+ @account_name = server.state[:account_name] or ""
349
+ @password = server.state[:password] or ""
350
+ @layout = Rfm::Factory::LayoutFactory.new(server, self)
351
+ @script = Rfm::Factory::ScriptFactory.new(server, self)
352
+ end
353
+
354
+ attr_reader :server, :name, :account_name, :password, :layout, :script
355
+ attr_writer :account_name, :password
356
+
357
+ # Access the Layout object representing a layout in this database. For example:
358
+ #
359
+ # myDatabase['Details']
360
+ #
361
+ # would return a Layout object representing the _Details_
362
+ # layout in the database.
363
+ #
364
+ # Note: RFM never talks to the server until you perform an action. The Layout object
365
+ # returned is created on the fly and assumed to refer to a valid layout, but you will
366
+ # get no error at this point if the layout you specify doesn't exist. Instead, you'll
367
+ # receive an error when you actually try to perform some action it.
368
+ def [](layout_name)
369
+ self.layout[layout_name]
370
+ end
371
+
372
+ end
373
+
374
+ # The Layout object represents a single FileMaker Pro layout. You use it to interact with
375
+ # records in FileMaker. *All* access to FileMaker data is done through a layout, and this
376
+ # layout determins which _table_ you actually hit (since every layout is explicitly associated
377
+ # with a particular table -- see FileMakers Layout->Layout Setup dialog box). You never specify
378
+ # _table_ information directly in RFM.
379
+ #
380
+ # Also, the layout determines which _fields_ will be returned. If a layout contains only three
381
+ # fields from a large table, only those three fields are returned. If a layout includes related
382
+ # fields from another table, they are returned as well. And if the layout includes portals, all
383
+ # data in the portals is returned (see Record::portal for details).
384
+ #
385
+ # As such, you can _significantly_ improve performance by limiting what you put on the layout.
386
+ #
387
+ # =Using Layouts
388
+ #
389
+ # The Layout object is where you get most of your work done. It includes methods for all
390
+ # FileMaker actions:
391
+ #
392
+ # * Layout::all
393
+ # * Layout::any
394
+ # * Layout::find
395
+ # * Layout::edit
396
+ # * Layout::create
397
+ # * Layout::delete
398
+ #
399
+ # =Running Scripts
400
+ #
401
+ # In FileMaker, execution of a script must accompany another action. For example, to run a script
402
+ # called _Remove Duplicates_ with a found set that includes everybody
403
+ # named _Bill_, do this:
404
+ #
405
+ # myLayout.find({"First Name" => "Bill"}, :post_script => "Remove Duplicates")
406
+ #
407
+ # ==Controlling When the Script Runs
408
+ #
409
+ # When you perform an action in FileMaker, it always executes in this order:
410
+ #
411
+ # 1. Perform any find
412
+ # 2. Sort the records
413
+ # 3. Return the results
414
+ #
415
+ # You can control when in the process the script runs. Each of these options is available:
416
+ #
417
+ # * *post_script* tells FileMaker to run the script after finding and sorting
418
+ # * *pre_find_script* tells FileMaker to run the script _before_ finding
419
+ # * *pre_sort_script* tells FileMaker to run the script _before_ sorting, but _after_ finding
420
+ #
421
+ # ==Passing Parameters to a Script
422
+ #
423
+ # If you want to pass a parameter to the script, use the options above, but supply an array value
424
+ # instead of a single string. For example:
425
+ #
426
+ # myLayout.find({"First Name" => "Bill"}, :post_script => ["Remove Duplicates", 10])
427
+ #
428
+ # This sample runs the script called "Remove Duplicates" and passes it the value +10+ as its
429
+ # script parameter.
430
+ #
431
+ # =Common Options
432
+ #
433
+ # Most of the methods on the Layout object accept an optional hash of +options+ to manipulate the
434
+ # action. For example, when you perform a find, you will typiclaly get back _all_ matching records.
435
+ # If you want to limit the number of records returned, you can do this:
436
+ #
437
+ # myLayout.find({"First Name" => "Bill"}, :max_records => 100)
438
+ #
439
+ # The +:max_records+ option tells FileMaker to limit the number of records returned.
440
+ #
441
+ # This is the complete list of available options:
442
+ #
443
+ # * *max_records* tells FileMaker how many records to return
444
+ #
445
+ # * *skip_records* tells FileMaker how many records in the found set to skip, before
446
+ # returning results; this is typically combined with +max_records+ to "page" through
447
+ # records
448
+ #
449
+ # * *sort_field* tells FileMaker to sort the records by the specified field
450
+ #
451
+ # * *sort_order* can be +desc+ (descending) or +asc+ (ascending) and determines the order
452
+ # of the sort when +sort_field+ is specified
453
+ #
454
+ # * *post_script* tells FileMaker to perform a script after carrying out the action; you
455
+ # can pass the script name, or a two-element array, with the script name first, then the
456
+ # script parameter
457
+ #
458
+ # * *pre_find_script* is like +post_script+ except the script runs before any find is
459
+ # performed
460
+ #
461
+ # * *pre_sort_script* is like +pre_find_script+ except the script runs after any find
462
+ # and before any sort
463
+ #
464
+ # * *response_layout* tells FileMaker to switch layouts before producing the response; this
465
+ # is useful when you need a field on a layout to perform a find, edit, or create, but you
466
+ # want to improve performance by not including the field in the result
467
+ #
468
+ # * *logical_operator* can be +and+ or +or+ and tells FileMaker how to process multiple fields
469
+ # in a find request
470
+ #
471
+ # * *modification_id* lets you pass in the modification id from a Record object with the request;
472
+ # when you do, the action will fail if the record was modified in FileMaker after it was retrieved
473
+ # by RFM but before the action was run
474
+ #
475
+ #
476
+ # =Attributes
477
+ #
478
+ # The Layout object has a few useful attributes:
479
+ #
480
+ # * +name+ is the name of the layout
481
+ #
482
+ # * +field_controls+ is a hash of FieldControl objects, with the field names as keys. FieldControl's
483
+ # tell you about the field on the layout: how is it formatted and what value list is assigned
484
+ #
485
+ # Note: It is possible to put the same field on a layout more than once. When this is the case, the
486
+ # value in +field_controls+ for that field is an array with one element representing each instance
487
+ # of the field.
488
+ #
489
+ # * +value_lists+ is a hash of arrays. The keys are value list names, and the values in the hash
490
+ # are arrays containing the actual value list items. +value_lists+ will include every value
491
+ # list that is attached to any field on the layout
492
+
493
+ class Layout
494
+
495
+ # Initialize a layout object. You never really need to do this. Instead, just do this:
496
+ #
497
+ # myServer = Rfm::Server.new(...)
498
+ # myDatabase = myServer["Customers"]
499
+ # myLayout = myDatabase["Details"]
500
+ #
501
+ # This sample code gets a layout object representing the Details layout in the Customers database
502
+ # on the FileMaker server.
503
+ #
504
+ # In case it isn't obvious, this is more easily expressed this way:
505
+ #
506
+ # myServer = Rfm::Server.new(...)
507
+ # myLayout = myServer["Customers"]["Details"]
508
+ def initialize(name, db)
509
+ @name = name
510
+ @db = db
511
+
512
+ @loaded = false
513
+ @field_controls = Rfm::Util::CaseInsensitiveHash.new
514
+ @value_lists = Rfm::Util::CaseInsensitiveHash.new
515
+ end
516
+
517
+ attr_reader :name, :db
518
+
519
+ def field_controls
520
+ load if !@loaded
521
+ @field_controls
522
+ end
523
+
524
+ def value_lists
525
+ load if !@loaded
526
+ @value_lists
527
+ end
528
+
529
+ # Returns a ResultSet object containing _every record_ in the table associated with this layout.
530
+ def all(options = {})
531
+ get_records('-findall', {}, options)
532
+ end
533
+
534
+ # Returns a ResultSet containing a single random record from the table associated with this layout.
535
+ def any(options = {})
536
+ get_records('-findany', {}, options)
537
+ end
538
+
539
+ # Finds a record. Typically you will pass in a hash of field names and values. For example:
540
+ #
541
+ # myLayout.find({"First Name" => "Bill"})
542
+ #
543
+ # Values in the hash work just like value in FileMaker's Find mode. You can use any special
544
+ # symbols (+==+, +...+, +>+, etc...).
545
+ #
546
+ # If you pass anything other than a hash as the first parameter, it is converted to a string and
547
+ # assumed to be FileMaker's internal id for a record (the recid).
548
+ def find(hash_or_recid, options = {})
549
+ if hash_or_recid.kind_of? Hash
550
+ get_records('-find', hash_or_recid, options)
551
+ else
552
+ get_records('-find', {'-recid' => hash_or_recid.to_s}, options)
553
+ end
554
+ end
555
+
556
+ # Updates the contents of the record whose internal +recid+ is specified. Send in a hash of new
557
+ # data in the +values+ parameter. Returns a RecordSet containing the modified record. For example:
558
+ #
559
+ # recid = myLayout.find({"First Name" => "Bill"})[0].record_id
560
+ # myLayout.edit(recid, {"First Name" => "Steve"})
561
+ #
562
+ # The above code would find the first record with _Bill_ in the First Name field and change the
563
+ # first name to _Steve_.
564
+ def edit(recid, values, options = {})
565
+ get_records('-edit', {'-recid' => recid}.merge(values), options)
566
+ end
567
+
568
+ # Creates a new record in the table associated with this layout. Pass field data as a hash in the
569
+ # +values+ parameter. Returns the newly created record in a RecordSet. You can use the returned
570
+ # record to, ie, discover the values in auto-enter fields (like serial numbers).
571
+ #
572
+ # For example:
573
+ #
574
+ # result = myLayout.create({"First Name" => "Jerry", "Last Name" => "Robin"})
575
+ # id = result[0]["ID"]
576
+ #
577
+ # The above code adds a new record with first name _Jerry_ and last name _Robin_. It then
578
+ # puts the value from the ID field (a serial number) into a ruby variable called +id+.
579
+ def create(values, options = {})
580
+ get_records('-new', values, options)
581
+ end
582
+
583
+ # Deletes the record with the specified internal recid. Returns a ResultSet with the deleted record.
584
+ #
585
+ # For example:
586
+ #
587
+ # recid = myLayout.find({"First Name" => "Bill"})[0].record_id
588
+ # myLayout.delete(recid)
589
+ #
590
+ # The above code finds every record with _Bill_ in the First Name field, then deletes the first one.
591
+ def delete(recid, options = {})
592
+ get_records('-delete', {'-recid' => recid}, options)
593
+ return nil
594
+ end
595
+
596
+ private
597
+
598
+ def load
599
+ @loaded = true
600
+ fmpxmllayout = @db.server.load_layout(self).body
601
+ doc = REXML::Document.new(fmpxmllayout)
602
+ root = doc.root
603
+
604
+ # check for errors
605
+ error = root.elements['ERRORCODE'].text.to_i
606
+ raise Rfm::Error::FileMakerError.getError(error) if error != 0
607
+
608
+ # process valuelists
609
+ if root.elements['VALUELISTS'].size > 0
610
+ root.elements['VALUELISTS'].each_element('VALUELIST') { |valuelist|
611
+ name = valuelist.attributes['NAME']
612
+ @value_lists[name] = valuelist.elements.collect {|e| e.text}
613
+ }
614
+ @value_lists.freeze
615
+ end
616
+
617
+ # process field controls
618
+ root.elements['LAYOUT'].each_element('FIELD') { |field|
619
+ name = field.attributes['NAME']
620
+ style = field.elements['STYLE'].attributes['TYPE']
621
+ value_list_name = field.elements['STYLE'].attributes['VALUELIST']
622
+ value_list = @value_lists[value_list_name] if value_list_name != ''
623
+ field_control = FieldControl.new(name, style, value_list_name, value_list)
624
+ existing = @field_controls[name]
625
+ if existing
626
+ if existing.kind_of?(Array)
627
+ existing << field_control
628
+ else
629
+ @field_controls[name] = Array[existing, field_control]
630
+ end
631
+ else
632
+ @field_controls[name] = field_control
633
+ end
634
+ }
635
+ @field_controls.freeze
636
+ end
637
+
638
+ def get_records(action, extra_params = {}, options = {})
639
+ Rfm::Result::ResultSet.new(
640
+ @db.server, @db.server.do_action(@db.account_name, @db.password, action, params().merge(extra_params), options).body,
641
+ self)
642
+ end
643
+
644
+ def params
645
+ {"-db" => @db.name, "-lay" => self.name}
646
+ end
647
+ end
648
+
649
+ # The FieldControl object represents a field on a FileMaker layout. You can find out what field
650
+ # style the field uses, and the value list attached to it.
651
+ #
652
+ # =Attributes
653
+ #
654
+ # * *name* is the name of the field
655
+ #
656
+ # * *style* is any one of:
657
+ # * * :edit_box - a normal editable field
658
+ # * * :scrollable - an editable field with scroll bar
659
+ # * * :popup_menu - a pop-up menu
660
+ # * * :checkbox_set - a set of checkboxes
661
+ # * * :radio_button_set - a set of radio buttons
662
+ # * * :popup_list - a pop-up list
663
+ # * * :calendar - a pop-up calendar
664
+ #
665
+ # * *value_list_name* is the name of the attached value list, if any
666
+ #
667
+ # * *value_list* is an array of strings representing the value list items, or nil
668
+ # if this field has no attached value list
669
+ class FieldControl
670
+ def initialize(name, style, value_list_name, value_list)
671
+ @name = name
672
+ case style
673
+ when "EDITTEXT"
674
+ @style = :edit_box
675
+ when "POPUPMENU"
676
+ @style = :popup_menu
677
+ when "CHECKBOX"
678
+ @style = :checkbox_set
679
+ when "RADIOBUTTONS"
680
+ @style = :radio_button_set
681
+ when "POPUPLIST"
682
+ @style = :popup_list
683
+ when "CALENDAR"
684
+ @style = :calendar
685
+ when "SCROLLTEXT"
686
+ @style = :scrollable
687
+ end
688
+ @value_list_name = value_list_name
689
+ @value_list = value_list
690
+ end
691
+
692
+ attr_reader :name, :style, :value_list_name, :value_list
693
+
694
+ end
695
+
696
+ # The Script object represents a FileMaker script. At this point, the Script object exists only so
697
+ # you can enumrate all scripts in a Database (which is a rare need):
698
+ #
699
+ # myDatabase.script.each {|script|
700
+ # puts script.name
701
+ # }
702
+ #
703
+ # If you want to _run_ a script, see the Layout object instead.
704
+ class Script
705
+ def initialize(name, db)
706
+ @name = name
707
+ @db = db
708
+ end
709
+
710
+ attr_reader :name
711
+ end
712
+
713
+ end