lardawge-rfm 1.3.1 → 1.4.0

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