rfm 0.2.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/rfm.rb +228 -1
- data/lib/rfm_command.rb +562 -29
- data/lib/rfm_error.rb +241 -14
- data/lib/rfm_factory.rb +14 -7
- data/lib/rfm_result.rb +268 -11
- data/lib/rfm_util.rb +10 -0
- data/tests/rfm_test_errors.rb +53 -0
- data/tests/rfm_tester.rb +2 -0
- metadata +6 -4
data/lib/rfm_error.rb
CHANGED
@@ -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
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
-
|
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
|
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
|
223
|
+
class NumberValidationError < ValidationError
|
18
224
|
end
|
19
225
|
|
20
|
-
class
|
226
|
+
class RangeValidationError < ValidationError
|
227
|
+
end
|
228
|
+
|
229
|
+
class UniqueValidationError < ValidationError
|
21
230
|
end
|
22
231
|
|
23
|
-
class
|
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
|
data/lib/rfm_factory.rb
CHANGED
@@ -1,5 +1,12 @@
|
|
1
|
-
module
|
2
|
-
|
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 <
|
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 <
|
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
|
}
|
data/lib/rfm_result.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
115
|
-
|
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 "
|
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("
|
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
|