lardawge-rfm 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/rfm_result.rb ADDED
@@ -0,0 +1,430 @@
1
+ # This module includes classes that represent FileMaker data. When you communicate with FileMaker
2
+ # using, ie, the Layout object, you typically get back ResultSet objects. These contain Records,
3
+ # which in turn contain Fields, Portals, and arrays of data.
4
+ #
5
+ # Author:: Geoff Coffey (mailto:gwcoffey@gmail.com)
6
+ # Copyright:: Copyright (c) 2007 Six Fried Rice, LLC and Mufaddal Khumri
7
+ # License:: See MIT-LICENSE for details
8
+ require 'bigdecimal'
9
+ require 'date'
10
+
11
+ module Rfm::Result
12
+
13
+ # The ResultSet object represents a set of records in FileMaker. It is, in every way, a real Ruby
14
+ # Array, so everything you expect to be able to do with an Array can be done with a ResultSet as well.
15
+ # In this case, the elements in the array are Record objects.
16
+ #
17
+ # Here's a typical example, displaying the results of a Find:
18
+ #
19
+ # myServer = Rfm::Server.new(...)
20
+ # results = myServer["Customers"]["Details"].find("First Name" => "Bill")
21
+ # results.each {|record|
22
+ # puts record["First Name"]
23
+ # puts record["Last Name"]
24
+ # puts record["Email Address"]
25
+ # }
26
+ #
27
+ # =Attributes
28
+ #
29
+ # The ResultSet object has several useful attributes:
30
+ #
31
+ # * *server* is the server object this ResultSet came from
32
+ #
33
+ # * *fields* is a hash with field names for keys and Field objects for values; it provides
34
+ # metadata about the fields in the ResultSet
35
+ #
36
+ # * *portals* is a hash with table occurrence names for keys and arrays of Field objects for values;
37
+ # it provides metadata about the portals in the ResultSet and the Fields on those portals
38
+
39
+ class ResultSet < Array
40
+
41
+ # Initializes a new ResultSet object. You will probably never do this your self (instead, use the Layout
42
+ # object to get various ResultSet obejects).
43
+ #
44
+ # If you feel so inclined, though, pass a Server object, and some +fmpxmlresult+ compliant XML in a String.
45
+ #
46
+ # =Attributes
47
+ #
48
+ # The ResultSet object includes several useful attributes:
49
+ #
50
+ # * *fields* is a hash (with field names for keys and Field objects for values). It includes an entry for
51
+ # every field in the ResultSet. Note: You don't use Field objects to access _data_. If you're after
52
+ # data, get a Record object (ResultSet is an array of records). Field objects tell you about the fields
53
+ # (their type, repetitions, and so forth) in case you find that information useful programmatically.
54
+ #
55
+ # Note: keys in the +fields+ hash are downcased for convenience (and [] automatically downcases on
56
+ # lookup, so it should be seamless). But if you +each+ a field hash and need to know a field's real
57
+ # name, with correct case, do +myField.name+ instead of relying on the key in the hash.
58
+ #
59
+ # * *portals* is a hash (with table occurrence names for keys and Field objects for values). If your
60
+ # layout contains portals, you can find out what fields they contain here. Again, if it's the data you're
61
+ # after, you want to look at the Record object.
62
+ def initialize(server, fmresultset, layout = nil)
63
+ @server = server
64
+ @resultset = nil
65
+ @layout = layout
66
+ @fields = Rfm::Util::CaseInsensitiveHash.new
67
+ @portals = Rfm::Util::CaseInsensitiveHash.new
68
+ @date_format = nil
69
+ @time_format = nil
70
+ @timestamp_format = nil
71
+
72
+ doc = REXML::Document.new(fmresultset)
73
+ root = doc.root
74
+
75
+ # check for errors
76
+ error = root.elements['error'].attributes['code'].to_i
77
+ if error != 0 && (error != 401 || @server.state[:raise_on_401])
78
+ raise Rfm::Error::FileMakerError.getError(error)
79
+ end
80
+
81
+ # ascertain date and time formats
82
+ datasource = root.elements['datasource']
83
+ @date_format = convertFormatString(datasource.attributes['date-format'])
84
+ @time_format = convertFormatString(datasource.attributes['time-format'])
85
+ @timestamp_format = convertFormatString(datasource.attributes['timestamp-format'])
86
+
87
+ # process field metadata
88
+ root.elements['metadata'].each_element('field-definition') { |field|
89
+ name = field.attributes['name']
90
+ @fields[name] = Field.new(self, field)
91
+ }
92
+ @fields.freeze
93
+
94
+ # process relatedset metadata
95
+ root.elements['metadata'].each_element('relatedset-definition') { |relatedset|
96
+ table = relatedset.attributes['table']
97
+ fields = {}
98
+ relatedset.each_element('field-definition') { |field|
99
+ name = field.attributes['name'].sub(Regexp.new(table + '::'), '')
100
+ fields[name] = Field.new(self, field)
101
+ }
102
+ @portals[table] = fields
103
+ }
104
+ @portals.freeze
105
+
106
+ # build rows
107
+ root.elements['resultset'].each_element('record') { |record|
108
+ self << Record.new(record, self, @fields, @layout)
109
+ }
110
+ end
111
+
112
+ attr_reader :server, :fields, :portals, :date_format, :time_format, :timestamp_format, :layout
113
+
114
+ private
115
+
116
+ def convertFormatString(fm_format)
117
+ fm_format.gsub('MM', '%m').gsub('dd', '%d').gsub('yyyy', '%Y').gsub('HH', '%H').gsub('mm', '%M').gsub('ss', '%S')
118
+ end
119
+
120
+ end
121
+
122
+ # The Record object represents a single FileMaker record. You typically get them from ResultSet objects.
123
+ # For example, you might use a Layout object to find some records:
124
+ #
125
+ # results = myLayout.find({"First Name" => "Bill"})
126
+ #
127
+ # The +results+ variable in this example now contains a ResultSet object. ResultSets are really just arrays of
128
+ # Record objects (with a little extra added in). So you can get a record object just like you would access any
129
+ # typical array element:
130
+ #
131
+ # first_record = results[0]
132
+ #
133
+ # You can find out how many record were returned:
134
+ #
135
+ # record_count = results.size
136
+ #
137
+ # And you can of course iterate:
138
+ #
139
+ # results.each (|record|
140
+ # // you can work with the record here
141
+ # )
142
+ #
143
+ # =Accessing Field Data
144
+ #
145
+ # You can access field data in the Record object in two ways. Typically, you simply treat Record like a hash
146
+ # (because it _is_ a hash...I love OOP). Keys are field names:
147
+ #
148
+ # first = myRecord["First Name"]
149
+ # last = myRecord["Last Name"]
150
+ #
151
+ # If your field naming conventions mean that your field names are also valid Ruby symbol named (ie: they contain only
152
+ # letters, numbers, and underscores) then you can treat them like attributes of the record. For example, if your fields
153
+ # are called "first_name" and "last_name" you can do this:
154
+ #
155
+ # first = myRecord.first_name
156
+ # last = myRecord.last_name
157
+ #
158
+ # Note: This shortcut will fail (in a rather mysterious way) if your field name happens to match any real attribute
159
+ # name of a Record object. For instance, you may have a field called "server". If you try this:
160
+ #
161
+ # server_name = myRecord.server
162
+ #
163
+ # you'll actually set +server_name+ to the Rfm::Server object this Record came from. This won't fail until you try
164
+ # to treat it as a String somewhere else in your code. It is also possible a future version of Rfm will include
165
+ # new attributes on the Record class which may clash with your field names. This will cause perfectly valid code
166
+ # today to fail later when you upgrade. If you can't stomach this kind of insanity, stick with the hash-like
167
+ # method of field access, which has none of these limitations. Also note that the +myRecord[]+ method is probably
168
+ # somewhat faster since it doesn't go through +method_missing+.
169
+ #
170
+ # =Accessing Repeating Fields
171
+ #
172
+ # If you have a repeating field, RFM simply returns an array:
173
+ #
174
+ # val1 = myRecord["Price"][0]
175
+ # val2 = myRecord["Price"][1]
176
+ #
177
+ # In the above example, the Price field is a repeating field. The code puts the first repetition in a variable called
178
+ # +val1+ and the second in a variable called +val2+.
179
+ #
180
+ # =Accessing Portals
181
+ #
182
+ # If the ResultSet includes portals (because the layout it comes from has portals on it) you can access them
183
+ # using the Record::portals attribute. It is a hash with table occurrence names for keys, and arrays of Record
184
+ # objects for values. In other words, you can do this:
185
+ #
186
+ # myRecord.portals["Orders"].each {|record|
187
+ # puts record["Order Number"]
188
+ # }
189
+ #
190
+ # This code iterates through the rows of the _Orders_ portal.
191
+ #
192
+ # =Field Types and Ruby Types
193
+ #
194
+ # RFM automatically converts data from FileMaker into a Ruby object with the most reasonable type possible. The
195
+ # type are mapped thusly:
196
+ #
197
+ # * *Text* fields are converted to Ruby String objects
198
+ #
199
+ # * *Number* fields are converted to Ruby BigDecimal objects (the basic Ruby numeric types have
200
+ # much less precision and range than FileMaker number fields)
201
+ #
202
+ # * *Date* fields are converted to Ruby Date objects
203
+ #
204
+ # * *Time* fields are converted to Ruby DateTime objects (you can ignore the date component)
205
+ #
206
+ # * *Timestamp* fields are converted to Ruby DateTime objects
207
+ #
208
+ # * *Container* fields are converted to Ruby URI objects
209
+ #
210
+ # =Attributes
211
+ #
212
+ # In addition to +portals+, the Record object has these useful attributes:
213
+ #
214
+ # * *record_id* is FileMaker's internal identifier for this record (_not_ any ID field you might have
215
+ # in your table); you need a +record_id+ to edit or delete a record
216
+ #
217
+ # * *mod_id* is the modification identifier for the record; whenever a record is modified, its +mod_id+
218
+ # changes so you can tell if the Record object you're looking at is up-to-date as compared to another
219
+ # copy of the same record
220
+ class Record < Rfm::Util::CaseInsensitiveHash
221
+
222
+ # Initializes a Record object. You really really never need to do this yourself. Instead, get your records
223
+ # from a ResultSet object.
224
+ def initialize(row_element, resultset, fields, layout, portal=nil)
225
+ @record_id = row_element.attributes['record-id']
226
+ @mod_id = row_element.attributes['mod-id']
227
+ @mods = {}
228
+ @resultset = resultset
229
+ @layout = layout
230
+
231
+ @loaded = false
232
+
233
+ row_element.each_element('field') { |field|
234
+ field_name = field.attributes['name']
235
+ field_name.sub!(Regexp.new(portal + '::'), '') if portal
236
+ datum = []
237
+ field.each_element('data') {|x| datum.push(fields[field_name].coerce(x.text))}
238
+ if datum.length == 1
239
+ self[field_name] = datum[0]
240
+ elsif datum.length == 0
241
+ self[field_name] = nil
242
+ else
243
+ self[field_name] = datum
244
+ end
245
+ }
246
+
247
+ @portals = Rfm::Util::CaseInsensitiveHash.new
248
+ row_element.each_element('relatedset') { |relatedset|
249
+ table = relatedset.attributes['table']
250
+ records = []
251
+ relatedset.each_element('record') { |record|
252
+ records << Record.new(record, @resultset, @resultset.portals[table], @layout, table)
253
+ }
254
+ @portals[table] = records
255
+ }
256
+
257
+ @loaded = true
258
+ end
259
+
260
+ attr_reader :record_id, :mod_id, :portals
261
+
262
+ # Saves local changes to the Record object back to Filemaker. For example:
263
+ #
264
+ # myLayout.find({"First Name" => "Bill"}).each(|record|
265
+ # record["First Name"] = "Steve"
266
+ # record.save
267
+ # )
268
+ #
269
+ # This code finds every record with _Bill_ in the First Name field, then changes the first name to
270
+ # Steve.
271
+ #
272
+ # Note: This method is smart enough to not bother saving if nothing has changed. So there's no need
273
+ # to optimize on your end. Just save, and if you've changed the record it will be saved. If not, no
274
+ # server hit is incurred.
275
+ def save
276
+ self.merge(@layout.edit(self.record_id, @mods)[0]) if @mods.size > 0
277
+ @mods.clear
278
+ end
279
+
280
+ # Like Record::save, except it fails (and raises an error) if the underlying record in FileMaker was
281
+ # modified after the record was fetched but before it was saved. In other words, prevents you from
282
+ # accidentally overwriting changes someone else made to the record.
283
+ def save_if_not_modified
284
+ self.merge(@layout.edit(@record_id, @mods, {:modification_id => @mod_id})[0]) if @mods.size > 0
285
+ @mods.clear
286
+ end
287
+
288
+ # Gets the value of a field from the record. For example:
289
+ #
290
+ # first = myRecord["First Name"]
291
+ # last = myRecord["Last Name"]
292
+ #
293
+ # This sample puts the first and last name from the record into Ruby variables.
294
+ #
295
+ # You can also update a field:
296
+ #
297
+ # myRecord["First Name"] = "Sophia"
298
+ #
299
+ # When you do, the change is noted, but *the data is not updated in FileMaker*. You must call
300
+ # Record::save or Record::save_if_not_modified to actually save the data.
301
+ def []=(pname, value)
302
+ return super if !@loaded # this just keeps us from getting mods during initialization
303
+ name = pname
304
+ if self[name] != nil
305
+ @mods[name] = val
306
+ else
307
+ raise Rfm::Error::ParameterError.new("You attempted to modify a field called '#{name}' on the Rfm::Record object, but that field does not exist.")
308
+ end
309
+ end
310
+
311
+ def method_missing (symbol, *attrs)
312
+ # check for simple getter
313
+ val = self[symbol.to_s]
314
+ return val if val != nil
315
+
316
+ # check for setter
317
+ symbol_name = symbol.to_s
318
+ if symbol_name[-1..-1] == '=' && self.has_key?(symbol_name[0..-2])
319
+ return @mods[symbol_name[0..-2]] = attrs[0]
320
+ end
321
+ super
322
+ end
323
+
324
+ def respond_to?(symbol, include_private = false)
325
+ return true if self[symbol.to_s] != nil
326
+ super
327
+ end
328
+ end
329
+
330
+ # The Field object represents a single FileMaker field. It *does not hold the data* in the field. Instead,
331
+ # it serves as a source of metadata about the field. For example, if you're script is trying to be highly
332
+ # dynamic about its field access, it may need to determine the data type of a field at run time. Here's
333
+ # how:
334
+ #
335
+ # field_name = "Some Field Name"
336
+ # case myRecord.fields[field_name].result
337
+ # when "text"
338
+ # # it is a text field, so handle appropriately
339
+ # when "number"
340
+ # # it is a number field, so handle appropriately
341
+ # end
342
+ #
343
+ # =Attributes
344
+ #
345
+ # The Field object has the following attributes useful attributes:
346
+ #
347
+ # * *name* is the name of the field
348
+ #
349
+ # * *result* is the data type of the field; possible values include:
350
+ # * text
351
+ # * number
352
+ # * date
353
+ # * time
354
+ # * timestamp
355
+ # * container
356
+ #
357
+ # * *type* any of these:
358
+ # * normal (a normal data field)
359
+ # * calculation
360
+ # * summary
361
+ #
362
+ # * *max_repeats* is the number of repetitions (1 for a normal field, more for a repeating field)
363
+ #
364
+ # * *global* is +true+ is this is a global field, *false* otherwise
365
+ #
366
+ # Note: Field types match FileMaker's own values, but the terminology differs. The +result+ attribute
367
+ # tells you the data type of the field, regardless of whether it is a calculation, summary, or normal
368
+ # field. So a calculation field whose result type is _timestamp_ would have these attributes:
369
+ #
370
+ # * result: timestamp
371
+ # * type: calculation
372
+ #
373
+ # * *control& is a FieldControl object representing the sytle and value list information associated
374
+ # with this field on the layout.
375
+ #
376
+ # Note: Since a field can sometimes appear on a layout more than once, +control+ may be an Array.
377
+ # If you don't know ahead of time, you'll need to deal with this. One easy way is:
378
+ #
379
+ # controls = [myField.control].flatten
380
+ # controls.each {|control|
381
+ # # do something with the control here
382
+ # }
383
+ #
384
+ # The code above makes sure the control is always an array. Typically, though, you'll know up front
385
+ # if the control is an array or not, and you can code accordingly.
386
+
387
+ class Field
388
+
389
+ # Initializes a field object. You'll never need to do this. Instead, get your Field objects from
390
+ # ResultSet::fields
391
+ def initialize(result_set, field)
392
+ @result_set = result_set
393
+ @name = field.attributes['name']
394
+ @result = field.attributes['result']
395
+ @type = field.attributes['type']
396
+ @max_repeats = field.attributes['max-repeats']
397
+ @global = field.attributes['global']
398
+
399
+ @laded = false
400
+ end
401
+
402
+ attr_reader :name, :result, :type, :max_repeats, :global
403
+
404
+ def control
405
+ @result_set.layout.field_controls[@name]
406
+ end
407
+
408
+ # Coerces the text value from an +fmresultset+ document into proper Ruby types based on the
409
+ # type of the field. You'll never need to do this: Rfm does it automatically for you when you
410
+ # access field data through the Record object.
411
+ def coerce(value)
412
+ return nil if (value == nil || value == '') && @result != "text"
413
+ case @result
414
+ when "text"
415
+ return value
416
+ when "number"
417
+ return BigDecimal.new(value)
418
+ when "date"
419
+ return Date.strptime(value, @result_set.date_format)
420
+ when "time"
421
+ return DateTime.strptime("1/1/-4712 " + value, "%m/%d/%Y #{@result_set.time_format}")
422
+ when "timestamp"
423
+ return DateTime.strptime(value, @result_set.timestamp_format)
424
+ when "container"
425
+ return URI.parse("#{@result_set.server.scheme}://#{@result_set.server.host_name}:#{@result_set.server.port}#{value}")
426
+ end
427
+ end
428
+ end
429
+
430
+ end
@@ -0,0 +1,10 @@
1
+ module Rfm::Util # :nodoc: all
2
+ class CaseInsensitiveHash < Hash
3
+ def []=(key, value)
4
+ super(key.downcase, value)
5
+ end
6
+ def [](key)
7
+ super(key.downcase)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,52 @@
1
+ require 'test/unit'
2
+ require '../lib/rfm'
3
+
4
+ # Test cases for testing the FileMakerError classes
5
+ #
6
+ # Author:: Mufaddal Khumri
7
+ # Copyright:: Copyright (c) 2007 Six Fried Rice, LLC and Mufaddal Khumri
8
+ # License:: See MIT-LICENSE for details
9
+ class TC_TestErrors < Test::Unit::TestCase
10
+
11
+ def test_default_message_system_errors
12
+ begin
13
+ raise Rfm::Error::FileMakerError.getError(0)
14
+ rescue Rfm::Error::SystemError => ex
15
+ assert_equal(ex.message, 'SystemError occurred.')
16
+ assert_equal(ex.code, 0)
17
+ end
18
+ end
19
+
20
+ def test_scriptmissing_errors
21
+ begin
22
+ raise Rfm::Error::FileMakerError.getError(104, 'ScriptMissingError occurred.')
23
+ rescue Rfm::Error::MissingError => ex
24
+ assert_equal(ex.code, 104)
25
+ end
26
+ end
27
+
28
+ def test_rangevalidation_errors
29
+ begin
30
+ raise Rfm::Error::FileMakerError.getError(503, 'RangeValidationError occurred.')
31
+ rescue Rfm::Error::ValidationError => ex
32
+ assert_equal(ex.code, 503)
33
+ end
34
+ end
35
+
36
+ def test_one_unknown_errors
37
+ begin
38
+ raise Rfm::Error::FileMakerError.getError(-1, 'UnknownError occurred.')
39
+ rescue Rfm::Error::UnknownError => ex
40
+ assert_equal(ex.code, -1)
41
+ end
42
+ end
43
+
44
+ def test_two_unknown_errors
45
+ begin
46
+ raise Rfm::Error::FileMakerError.getError(99999, 'UnknownError occurred.')
47
+ rescue Rfm::Error::UnknownError => ex
48
+ assert_equal(ex.code, 99999)
49
+ end
50
+ end
51
+
52
+ end
@@ -0,0 +1,2 @@
1
+ require 'test/unit'
2
+ require 'rfm_test_errors'
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lardawge-rfm
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Geoff Coffey
8
+ - Mufaddal Khumri
9
+ - Atsushi Matsuo
10
+ - Larry Sprock
11
+ autorequire:
12
+ bindir: bin
13
+ cert_chain: []
14
+
15
+ date: 2009-05-29 00:00:00 -07:00
16
+ default_executable:
17
+ dependencies:
18
+ - !ruby/object:Gem::Dependency
19
+ name: hpricot
20
+ type: :runtime
21
+ version_requirement:
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 0.8.1
27
+ version:
28
+ description: Rfm brings your FileMaker data to Ruby with elegance and speed. Now your Ruby scripts and Rails applications can talk directly to your FileMaker server with a syntax that just feels right.
29
+ email: http://groups.google.com/group/rfmcommunity
30
+ executables: []
31
+
32
+ extensions: []
33
+
34
+ extra_rdoc_files:
35
+ - README.rdoc
36
+ files:
37
+ - lib/rfm.rb
38
+ - lib/rfm_command.rb
39
+ - lib/rfm_error.rb
40
+ - lib/rfm_factory.rb
41
+ - lib/rfm_result.rb
42
+ - lib/rfm_utility.rb
43
+ - README.rdoc
44
+ has_rdoc: false
45
+ homepage: http://sixfriedrice.com/wp/products/rfm/
46
+ post_install_message:
47
+ rdoc_options:
48
+ - --charset=UTF-8
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: "0"
56
+ version:
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: "0"
62
+ version:
63
+ requirements: []
64
+
65
+ rubyforge_project:
66
+ rubygems_version: 1.2.0
67
+ signing_key:
68
+ specification_version: 3
69
+ summary: FileMaker to Ruby adapter
70
+ test_files:
71
+ - test/rfm_test_errors.rb
72
+ - test/rfm_tester.rb