lardawge-rfm 1.4.0 → 1.4.1

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