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.
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