rfm 0.2.0 → 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.
@@ -1,25 +1,252 @@
1
+ require "set"
2
+
3
+ # These classes wrap the filemaker error codes. FileMakerError is the base class of this hierarchy.
4
+ #
5
+ # One could get a FileMakerError by doing:
6
+ # err = Rfm::Error::FileMakerError.getError(102)
7
+ #
8
+ # The above code would return a FieldMissingError instance. Your could use this instance to raise that appropriate
9
+ # exception:
10
+ #
11
+ # raise err
12
+ #
13
+ # You could access the specific error code by accessing:
14
+ #
15
+ # err.code
16
+ #
17
+ # Author:: Mufaddal Khumri
18
+ # Copyright:: Copyright (c) 2007 Six Fried Rice, LLC and Mufaddal Khumri
19
+ # License:: See MIT-LICENSE for details
1
20
  module Rfm::Error
2
- class FileMakerError < Exception
3
- def self.instance(error_code)
4
- case error_code
5
- when 101 then return RecordMissingError.new
6
- when 102 then return FieldMissingError.new
7
- when 104 then return RelationshipMissingError.new
8
- when 105 then return LayoutMissingError.new
9
- else return FileMakerError.new
21
+
22
+ class RfmError < StandardError
23
+ end
24
+
25
+ class CommunicationError < RfmError
26
+ end
27
+
28
+ class ParameterError < RfmError
29
+ end
30
+
31
+ class AuthenticationError < RfmError
32
+ end
33
+
34
+ # Base class for all FileMaker errors
35
+ class FileMakerError < RfmError
36
+ attr_accessor :code
37
+
38
+ # Default filemaker error message map
39
+ @@default_messages = {}
40
+
41
+ # This method instantiates and returns the appropriate FileMakerError object depending on the error code passed to it. It
42
+ # also accepts an optional message.
43
+ def self.getError(code, message = nil)
44
+ if @@default_messages == nil or @@default_messages.size == 0
45
+ (0..99).each{|i| @@default_messages[i] = 'SystemError occurred.'}
46
+ (100..199).each{|i| @@default_messages[i] = 'MissingError occurred.'}
47
+ @@default_messages[102] = 'FieldMissingError occurred.'
48
+ @@default_messages[104] = 'ScriptMissingError occurred.'
49
+ @@default_messages[105] = 'LayoutMissingError occurred.'
50
+ @@default_messages[106] = 'TableMissingError occurred.'
51
+ (200..299).each{|i| @@default_messages[i] = 'SecurityError occurred.'}
52
+ @@default_messages[200] = 'RecordAccessDeniedError occurred.'
53
+ @@default_messages[201] = 'FieldCannotBeModifiedError occurred.'
54
+ @@default_messages[202] = 'FieldAccessIsDeniedError occurred.'
55
+ (300..399).each{|i| @@default_messages[i] = 'ConcurrencyError occurred.'}
56
+ @@default_messages[301] = 'RecordInUseError occurred.'
57
+ @@default_messages[302] = 'TableInUseError occurred.'
58
+ @@default_messages[306] = 'RecordModIdDoesNotMatchError occurred.'
59
+ (400..499).each{|i| @@default_messages[i] = 'GeneralError occurred.'}
60
+ @@default_messages[401] = 'NoRecordsFoundError occurred.'
61
+ (500..599).each{|i| @@default_messages[i] = 'ValidationError occurred.'}
62
+ @@default_messages[500] = 'DateValidationError occurred.'
63
+ @@default_messages[501] = 'TimeValidationError occurred.'
64
+ @@default_messages[502] = 'NumberValidationError occurred.'
65
+ @@default_messages[503] = 'RangeValidationError occurred.'
66
+ @@default_messages[504] = 'UniqueValidationError occurred.'
67
+ @@default_messages[505] = 'ExistingValidationError occurred.'
68
+ @@default_messages[506] = 'ValueListValidationError occurred.'
69
+ @@default_messages[507] = 'ValidationCalculationError occurred.'
70
+ @@default_messages[508] = 'InvalidFindModeValueError occurred.'
71
+ @@default_messages[511] = 'MaximumCharactersValidationError occurred.'
72
+ (800..899).each{|i| @@default_messages[i] = 'FileError occurred.'}
73
+ @@default_messages[802] = 'UnableToOpenFileError occurred.'
74
+ end
75
+
76
+ message = @@default_messages[code] if message == nil || message.strip == ''
77
+ message += " (FileMaker Error ##{code})"
78
+
79
+ if 0 <= code and code <= 99
80
+ err = SystemError.new(message)
81
+ elsif 100 <= code and code <= 199
82
+ if code == 101
83
+ err = RecordMissingError.new(message)
84
+ elsif code == 102
85
+ err = FieldMissingError.new(message)
86
+ elsif code == 104
87
+ err = ScriptMissingError.new(message)
88
+ elsif code == 105
89
+ err = LayoutMissingError.new(message)
90
+ elsif code == 106
91
+ err = TableMissingError.new(message)
92
+ else
93
+ err = MissingError.new(message)
94
+ end
95
+ elsif 200 <= code and code <= 299
96
+ if code == 200
97
+ err = RecordAccessDeniedError.new(message)
98
+ elsif code == 201
99
+ err = FieldCannotBeModifiedError.new(message)
100
+ elsif code == 202
101
+ err = FieldAccessIsDeniedError.new(message)
102
+ else
103
+ err = SecurityError.new(message)
104
+ end
105
+ elsif 300 <= code and code <= 399
106
+ if code == 301
107
+ err = RecordInUseError.new(message)
108
+ elsif code == 302
109
+ err = TableInUseError.new(message)
110
+ elsif code == 306
111
+ err = RecordModIdDoesNotMatchError.new(message)
112
+ else
113
+ err = ConcurrencyError.new(message)
114
+ end
115
+ elsif 400 <= code and code <= 499
116
+ if code == 401
117
+ err = NoRecordsFoundError.new(message)
118
+ else
119
+ err = GeneralError.new(message)
120
+ end
121
+ elsif 500 <= code and code <= 599
122
+ if code == 500
123
+ err = DateValidationError.new(message)
124
+ elsif code == 501
125
+ err = TimeValidationError.new(message)
126
+ elsif code == 502
127
+ err = NumberValidationError.new(message)
128
+ elsif code == 503
129
+ err = RangeValidationError.new(message)
130
+ elsif code == 504
131
+ err = UniqueValidationError.new(message)
132
+ elsif code == 505
133
+ err = ExistingValidationError.new(message)
134
+ elsif code == 506
135
+ err = ValueListValidationError.new(message)
136
+ elsif code == 507
137
+ err = ValidationCalculationError.new(message)
138
+ elsif code == 508
139
+ err = InvalidFindModeValueError.new(message)
140
+ elsif code == 511
141
+ err = MaximumCharactersValidationError.new(message)
142
+ else
143
+ err = ValidationError.new(message)
144
+ end
145
+ elsif 800 <= code and code <= 899
146
+ if code == 802
147
+ err = UnableToOpenFileError.new(message)
148
+ else
149
+ err = FileError.new(message)
150
+ end
151
+ else
152
+ # called for code == -1 or any other code not handled above.
153
+ err = UnknownError.new(message)
10
154
  end
11
- end
155
+ err.code = code
156
+ return err
157
+ end
158
+ end
159
+
160
+ class UnknownError < FileMakerError
161
+ end
162
+
163
+ class SystemError < FileMakerError
164
+ end
165
+
166
+ class MissingError < FileMakerError
167
+ end
168
+
169
+ class RecordMissingError < MissingError
170
+ end
171
+
172
+ class FieldMissingError < MissingError
173
+ end
174
+
175
+ class ScriptMissingError < MissingError
176
+ end
177
+
178
+ class LayoutMissingError < MissingError
179
+ end
180
+
181
+ class TableMissingError < MissingError
182
+ end
183
+
184
+ class SecurityError < FileMakerError
185
+ end
186
+
187
+ class RecordAccessDeniedError < SecurityError
188
+ end
189
+
190
+ class FieldCannotBeModifiedError < SecurityError
191
+ end
192
+
193
+ class FieldAccessIsDeniedError < SecurityError
194
+ end
195
+
196
+ class ConcurrencyError < FileMakerError
12
197
  end
13
198
 
14
- class RecordMissingError < FileMakerError
199
+ class RecordInUseError < ConcurrencyError
200
+ end
201
+
202
+ class TableInUseError < ConcurrencyError
203
+ end
204
+
205
+ class RecordModIdDoesNotMatchError < ConcurrencyError
206
+ end
207
+
208
+ class GeneralError < FileMakerError
209
+ end
210
+
211
+ class NoRecordsFoundError < GeneralError
212
+ end
213
+
214
+ class ValidationError < FileMakerError
215
+ end
216
+
217
+ class DateValidationError < ValidationError
218
+ end
219
+
220
+ class TimeValidationError < ValidationError
15
221
  end
16
222
 
17
- class FieldMissingError < FileMakerError
223
+ class NumberValidationError < ValidationError
18
224
  end
19
225
 
20
- class RelationshipMissingError < FileMakerError
226
+ class RangeValidationError < ValidationError
227
+ end
228
+
229
+ class UniqueValidationError < ValidationError
21
230
  end
22
231
 
23
- class LayoutMissingError < FileMakerError
232
+ class ExistingValidationError < ValidationError
233
+ end
234
+
235
+ class ValueListValidationError < ValidationError
236
+ end
237
+
238
+ class ValidationCalculationError < ValidationError
239
+ end
240
+
241
+ class InvalidFindModeValueError < ValidationError
242
+ end
243
+
244
+ class MaximumCharactersValidationError < ValidationError
245
+ end
246
+
247
+ class FileError < FileMakerError
248
+ end
249
+
250
+ class UnableToOpenFileError < FileError
24
251
  end
25
- end
252
+ end
@@ -1,5 +1,12 @@
1
- module Rfm::Factory
2
- class DbFactory < Hash
1
+ # The classes in this module are used internally by RFM and are not intended for outside
2
+ # use.
3
+ #
4
+ # Author:: Geoff Coffey (mailto:gwcoffey@gmail.com)
5
+ # Copyright:: Copyright (c) 2007 Six Fried Rice, LLC and Mufaddal Khumri
6
+ # License:: See MIT-LICENSE for details
7
+
8
+ module Rfm::Factory # :nodoc: all
9
+ class DbFactory < Rfm::Util::CaseInsensitiveHash
3
10
 
4
11
  def initialize(server)
5
12
  @server = server
@@ -12,7 +19,7 @@ module Rfm::Factory
12
19
 
13
20
  def all
14
21
  if !@loaded
15
- Rfm::Result::ResultSet.new(@server, @server.do_action('-dbnames', {}).body).each {|record|
22
+ Rfm::Result::ResultSet.new(@server, @server.do_action(@server.state[:account_name], @server.state[:password], '-dbnames', {}).body).each {|record|
16
23
  name = record['DATABASE_NAME']
17
24
  self[name] = Rfm::Database.new(name, @server) if self[name] == nil
18
25
  }
@@ -23,7 +30,7 @@ module Rfm::Factory
23
30
 
24
31
  end
25
32
 
26
- class LayoutFactory < Hash
33
+ class LayoutFactory < Rfm::Util::CaseInsensitiveHash
27
34
 
28
35
  def initialize(server, database)
29
36
  @server = server
@@ -37,7 +44,7 @@ module Rfm::Factory
37
44
 
38
45
  def all
39
46
  if !@loaded
40
- Rfm::Result::ResultSet.new(@server, @server.do_action('-layoutnames', {"-db" => @database.name}).body).each {|record|
47
+ Rfm::Result::ResultSet.new(@server, @server.do_action(@server.state[:account_name], @server.state[:password], '-layoutnames', {"-db" => @database.name}).body).each {|record|
41
48
  name = record['LAYOUT_NAME']
42
49
  self[name] = Rfm::Layout.new(name, @database) if self[name] == nil
43
50
  }
@@ -48,7 +55,7 @@ module Rfm::Factory
48
55
 
49
56
  end
50
57
 
51
- class ScriptFactory < Hash
58
+ class ScriptFactory < Rfm::Util::CaseInsensitiveHash
52
59
 
53
60
  def initialize(server, database)
54
61
  @server = server
@@ -62,7 +69,7 @@ module Rfm::Factory
62
69
 
63
70
  def all
64
71
  if !@loaded
65
- Rfm::Result::ResultSet.new(@server, @server.do_action('-scriptnames', {"-db" => @database.name}).body).each {|record|
72
+ Rfm::Result::ResultSet.new(@server, @server.do_action(@server.state[:account_name], @server.state[:password], '-scriptnames', {"-db" => @database.name}).body).each {|record|
66
73
  name = record['SCRIPT_NAME']
67
74
  self[name] = Rfm::Script.new(name, @database) if self[name] == nil
68
75
  }
@@ -1,15 +1,70 @@
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
1
8
  require 'bigdecimal'
2
9
  require 'date'
3
10
 
4
11
  module Rfm::Result
5
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
+
6
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.
7
62
  def initialize(server, fmresultset, layout = nil)
8
63
  @server = server
9
64
  @resultset = nil
10
65
  @layout = layout
11
- @fields = {}
12
- @portals = {}
66
+ @fields = Rfm::Util::CaseInsensitiveHash.new
67
+ @portals = Rfm::Util::CaseInsensitiveHash.new
13
68
  @date_format = nil
14
69
  @time_format = nil
15
70
  @timestamp_format = nil
@@ -19,7 +74,9 @@ module Rfm::Result
19
74
 
20
75
  # check for errors
21
76
  error = root.elements['error'].attributes['code'].to_i
22
- raise "Error #{error} occurred while processing the request" if error != 0 && (error != 401 || @server.state[:raise_on_401])
77
+ if error != 0 && (error != 401 || @server.state[:raise_on_401])
78
+ raise Rfm::Error::FileMakerError.getError(error)
79
+ end
23
80
 
24
81
  # ascertain date and time formats
25
82
  datasource = root.elements['datasource']
@@ -52,7 +109,7 @@ module Rfm::Result
52
109
  }
53
110
  end
54
111
 
55
- attr_reader :server, :fields, :portals, :date_format, :time_format, :timestamp_format
112
+ attr_reader :server, :fields, :portals, :date_format, :time_format, :timestamp_format, :layout
56
113
 
57
114
  private
58
115
 
@@ -62,7 +119,108 @@ module Rfm::Result
62
119
 
63
120
  end
64
121
 
65
- class Record < Hash
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.
66
224
  def initialize(row_element, resultset, fields, layout, portal=nil)
67
225
  @record_id = row_element.attributes['record-id']
68
226
  @mod_id = row_element.attributes['mod-id']
@@ -86,7 +244,7 @@ module Rfm::Result
86
244
  end
87
245
  }
88
246
 
89
- @portals = {}
247
+ @portals = Rfm::Util::CaseInsensitiveHash.new
90
248
  row_element.each_element('relatedset') { |relatedset|
91
249
  table = relatedset.attributes['table']
92
250
  records = []
@@ -101,22 +259,52 @@ module Rfm::Result
101
259
 
102
260
  attr_reader :record_id, :mod_id, :portals
103
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.
104
275
  def save
105
276
  self.merge(@layout.edit(self.record_id, @mods)[0]) if @mods.size > 0
106
277
  @mods.clear
107
278
  end
108
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.
109
283
  def save_if_not_modified
110
284
  self.merge(@layout.edit(@record_id, @mods, {:modification_id => @mod_id})[0]) if @mods.size > 0
111
285
  @mods.clear
112
286
  end
113
287
 
114
- def []=(name, value)
115
- return super if !@loaded
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
116
304
  if self[name] != nil
117
305
  @mods[name] = val
118
306
  else
119
- raise "No such field: #{name}"
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.")
120
308
  end
121
309
  end
122
310
 
@@ -139,7 +327,67 @@ module Rfm::Result
139
327
  end
140
328
  end
141
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
+
142
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
143
391
  def initialize(result_set, field)
144
392
  @result_set = result_set
145
393
  @name = field.attributes['name']
@@ -147,10 +395,19 @@ module Rfm::Result
147
395
  @type = field.attributes['type']
148
396
  @max_repeats = field.attributes['max-repeats']
149
397
  @global = field.attributes['global']
398
+
399
+ @laded = false
150
400
  end
151
401
 
152
- attr_reader :name, :type, :max_repeats, :global
402
+ attr_reader :name, :result, :type, :max_repeats, :global
403
+
404
+ def control
405
+ @result_set.layout.field_controls[@name]
406
+ end
153
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.
154
411
  def coerce(value)
155
412
  return nil if (value == nil || value == '') && @result != "text"
156
413
  case @result
@@ -165,7 +422,7 @@ module Rfm::Result
165
422
  when "timestamp"
166
423
  return DateTime.strptime(value, @result_set.timestamp_format)
167
424
  when "container"
168
- return URI.parse("http://#{@result_set.server.host_name}:#{@result_set.server.port}#{value}")
425
+ return URI.parse("#{@result_set.server.scheme}://#{@result_set.server.host_name}:#{@result_set.server.port}#{value}")
169
426
  end
170
427
  end
171
428
  end