rfm 0.2.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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