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.
- 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
|