ginjo-rfm 1.4.2
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/LICENSE +21 -0
- data/README.md +329 -0
- data/lib/rfm.rb +32 -0
- data/lib/rfm/database.rb +96 -0
- data/lib/rfm/error.rb +186 -0
- data/lib/rfm/layout.rb +291 -0
- data/lib/rfm/metadata/field.rb +93 -0
- data/lib/rfm/metadata/field_control.rb +53 -0
- data/lib/rfm/metadata/script.rb +20 -0
- data/lib/rfm/metadata/value_list_item.rb +27 -0
- data/lib/rfm/record.rb +232 -0
- data/lib/rfm/resultset.rb +137 -0
- data/lib/rfm/server.rb +395 -0
- data/lib/rfm/utilities/case_insensitive_hash.rb +10 -0
- data/lib/rfm/utilities/factory.rb +85 -0
- data/lib/rfm/version.rb +13 -0
- metadata +100 -0
data/lib/rfm/error.rb
ADDED
@@ -0,0 +1,186 @@
|
|
1
|
+
module Rfm
|
2
|
+
|
3
|
+
# Error is the base for the error hierarchy representing errors returned by Filemaker.
|
4
|
+
#
|
5
|
+
# One could raise a FileMakerError by doing:
|
6
|
+
# raise Rfm::Error.getError(102)
|
7
|
+
#
|
8
|
+
# It also takes an optional argument to give a more discriptive error message:
|
9
|
+
# err = Rfm::Error.getError(102, 'add description with more detail here')
|
10
|
+
#
|
11
|
+
# The above code would return a FieldMissing instance. Your could use this instance to raise that appropriate
|
12
|
+
# exception:
|
13
|
+
#
|
14
|
+
# raise err
|
15
|
+
#
|
16
|
+
# You could access the specific error code by accessing:
|
17
|
+
#
|
18
|
+
# err.code
|
19
|
+
module Error
|
20
|
+
|
21
|
+
class RfmError < StandardError #:nodoc:
|
22
|
+
attr_reader :code
|
23
|
+
|
24
|
+
def initialize(code, message=nil)
|
25
|
+
@code = code
|
26
|
+
super(message)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class UnknownError < RfmError
|
31
|
+
end
|
32
|
+
|
33
|
+
class SystemError < RfmError
|
34
|
+
end
|
35
|
+
|
36
|
+
class MissingError < RfmError
|
37
|
+
end
|
38
|
+
|
39
|
+
class RecordMissingError < MissingError #:nodoc:
|
40
|
+
end
|
41
|
+
|
42
|
+
class FieldMissingError < MissingError #:nodoc:
|
43
|
+
end
|
44
|
+
|
45
|
+
class ScriptMissingError < MissingError #:nodoc:
|
46
|
+
end
|
47
|
+
|
48
|
+
class LayoutMissingError < MissingError #:nodoc:
|
49
|
+
end
|
50
|
+
|
51
|
+
class TableMissingError < MissingError #:nodoc:
|
52
|
+
end
|
53
|
+
|
54
|
+
class SecurityError < RfmError #:nodoc:
|
55
|
+
end
|
56
|
+
|
57
|
+
class RecordAccessDeniedError < SecurityError #:nodoc:
|
58
|
+
end
|
59
|
+
|
60
|
+
class FieldCannotBeModifiedError < SecurityError #:nodoc:
|
61
|
+
end
|
62
|
+
|
63
|
+
class FieldAccessIsDeniedError < SecurityError #:nodoc:
|
64
|
+
end
|
65
|
+
|
66
|
+
class ConcurrencyError < RfmError #:nodoc:
|
67
|
+
end
|
68
|
+
|
69
|
+
class RecordInUseError < ConcurrencyError #:nodoc:
|
70
|
+
end
|
71
|
+
|
72
|
+
class TableInUseError < ConcurrencyError #:nodoc:
|
73
|
+
end
|
74
|
+
|
75
|
+
class RecordModIdDoesNotMatchError < ConcurrencyError #:nodoc:
|
76
|
+
end
|
77
|
+
|
78
|
+
class GeneralError < RfmError #:nodoc:
|
79
|
+
end
|
80
|
+
|
81
|
+
class NoRecordsFoundError < GeneralError #:nodoc:
|
82
|
+
end
|
83
|
+
|
84
|
+
class ValidationError < RfmError #:nodoc:
|
85
|
+
end
|
86
|
+
|
87
|
+
class DateValidationError < ValidationError #:nodoc:
|
88
|
+
end
|
89
|
+
|
90
|
+
class TimeValidationError < ValidationError #:nodoc:
|
91
|
+
end
|
92
|
+
|
93
|
+
class NumberValidationError < ValidationError #:nodoc:
|
94
|
+
end
|
95
|
+
|
96
|
+
class RangeValidationError < ValidationError #:nodoc:
|
97
|
+
end
|
98
|
+
|
99
|
+
class UniqueValidationError < ValidationError #:nodoc:
|
100
|
+
end
|
101
|
+
|
102
|
+
class ExistingValidationError < ValidationError #:nodoc:
|
103
|
+
end
|
104
|
+
|
105
|
+
class ValueListValidationError < ValidationError #:nodoc:
|
106
|
+
end
|
107
|
+
|
108
|
+
class ValidationCalculationError < ValidationError #:nodoc:
|
109
|
+
end
|
110
|
+
|
111
|
+
class InvalidFindModeValueError < ValidationError #:nodoc:
|
112
|
+
end
|
113
|
+
|
114
|
+
class MaximumCharactersValidationError < ValidationError #:nodoc:
|
115
|
+
end
|
116
|
+
|
117
|
+
class FileError < RfmError #:nodoc:
|
118
|
+
end
|
119
|
+
|
120
|
+
class UnableToOpenFileError < FileError #:nodoc:
|
121
|
+
end
|
122
|
+
|
123
|
+
extend self
|
124
|
+
# This method returns the appropriate FileMaker object depending on the error code passed to it. It
|
125
|
+
# also accepts an optional message.
|
126
|
+
def getError(code, message=nil)
|
127
|
+
klass = find_by_code(code)
|
128
|
+
message = build_message(klass, code, message)
|
129
|
+
error = klass.new(code, message)
|
130
|
+
error
|
131
|
+
end
|
132
|
+
|
133
|
+
def build_message(klass, code, message=nil) #:nodoc:
|
134
|
+
msg = ": #{message}"
|
135
|
+
msg << " " unless message.nil?
|
136
|
+
msg << "(FileMaker Error ##{code})"
|
137
|
+
|
138
|
+
"#{klass.to_s.gsub(/Rfm::Error::/, '')} occurred#{msg}"
|
139
|
+
end
|
140
|
+
|
141
|
+
def find_by_code(code) #:nodoc:
|
142
|
+
case code
|
143
|
+
when 0..99 then SystemError
|
144
|
+
when 100..199
|
145
|
+
if code == 101; RecordMissingError
|
146
|
+
elsif code == 102; FieldMissingError
|
147
|
+
elsif code == 104; ScriptMissingError
|
148
|
+
elsif code == 105; LayoutMissingError
|
149
|
+
elsif code == 106; TableMissingError
|
150
|
+
else; MissingError; end
|
151
|
+
when 203..299
|
152
|
+
if code == 200; RecordAccessDeniedError
|
153
|
+
elsif code == 201; FieldCannotBeModifiedError
|
154
|
+
elsif code == 202; FieldAccessIsDeniedError
|
155
|
+
else; SecurityError; end
|
156
|
+
when 300..399
|
157
|
+
if code == 301; RecordInUseError
|
158
|
+
elsif code == 302; TableInUseError
|
159
|
+
elsif code == 306; RecordModIdDoesNotMatchError
|
160
|
+
else; ConcurrencyError; end
|
161
|
+
when 400..499
|
162
|
+
if code == 401; NoRecordsFoundError
|
163
|
+
else; GeneralError; end
|
164
|
+
when 500..599
|
165
|
+
if code == 500; DateValidationError
|
166
|
+
elsif code == 501; TimeValidationError
|
167
|
+
elsif code == 502; NumberValidationError
|
168
|
+
elsif code == 503; RangeValidationError
|
169
|
+
elsif code == 504; UniqueValidationError
|
170
|
+
elsif code == 505; ExistingValidationError
|
171
|
+
elsif code == 506; ValueListValidationError
|
172
|
+
elsif code == 507; ValidationCalculationError
|
173
|
+
elsif code == 508; InvalidFindModeValueError
|
174
|
+
elsif code == 511; MaximumCharactersValidationError
|
175
|
+
else; ValidationError
|
176
|
+
end
|
177
|
+
when 800..899
|
178
|
+
if code == 802; UnableToOpenFileError
|
179
|
+
else; FileError; end
|
180
|
+
else
|
181
|
+
UnknownError
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
end
|
data/lib/rfm/layout.rb
ADDED
@@ -0,0 +1,291 @@
|
|
1
|
+
module Rfm
|
2
|
+
# The Layout object represents a single FileMaker Pro layout. You use it to interact with
|
3
|
+
# records in FileMaker. *All* access to FileMaker data is done through a layout, and this
|
4
|
+
# layout determines which _table_ you actually hit (since every layout is explicitly associated
|
5
|
+
# with a particular table -- see FileMakers Layout->Layout Setup dialog box). You never specify
|
6
|
+
# _table_ information directly in RFM.
|
7
|
+
#
|
8
|
+
# Also, the layout determines which _fields_ will be returned. If a layout contains only three
|
9
|
+
# fields from a large table, only those three fields are returned. If a layout includes related
|
10
|
+
# fields from another table, they are returned as well. And if the layout includes portals, all
|
11
|
+
# data in the portals is returned (see Record::portal for details).
|
12
|
+
#
|
13
|
+
# As such, you can _significantly_ improve performance by limiting what you put on the layout.
|
14
|
+
#
|
15
|
+
# =Using Layouts
|
16
|
+
#
|
17
|
+
# The Layout object is where you get most of your work done. It includes methods for all
|
18
|
+
# FileMaker actions:
|
19
|
+
#
|
20
|
+
# * Layout::all
|
21
|
+
# * Layout::any
|
22
|
+
# * Layout::find
|
23
|
+
# * Layout::edit
|
24
|
+
# * Layout::create
|
25
|
+
# * Layout::delete
|
26
|
+
#
|
27
|
+
# =Running Scripts
|
28
|
+
#
|
29
|
+
# In FileMaker, execution of a script must accompany another action. For example, to run a script
|
30
|
+
# called _Remove Duplicates_ with a found set that includes everybody
|
31
|
+
# named _Bill_, do this:
|
32
|
+
#
|
33
|
+
# myLayout.find({"First Name" => "Bill"}, :post_script => "Remove Duplicates")
|
34
|
+
#
|
35
|
+
# ==Controlling When the Script Runs
|
36
|
+
#
|
37
|
+
# When you perform an action in FileMaker, it always executes in this order:
|
38
|
+
#
|
39
|
+
# 1. Perform any find
|
40
|
+
# 2. Sort the records
|
41
|
+
# 3. Return the results
|
42
|
+
#
|
43
|
+
# You can control when in the process the script runs. Each of these options is available:
|
44
|
+
#
|
45
|
+
# * *post_script* tells FileMaker to run the script after finding and sorting
|
46
|
+
# * *pre_find_script* tells FileMaker to run the script _before_ finding
|
47
|
+
# * *pre_sort_script* tells FileMaker to run the script _before_ sorting, but _after_ finding
|
48
|
+
#
|
49
|
+
# ==Passing Parameters to a Script
|
50
|
+
#
|
51
|
+
# If you want to pass a parameter to the script, use the options above, but supply an array value
|
52
|
+
# instead of a single string. For example:
|
53
|
+
#
|
54
|
+
# myLayout.find({"First Name" => "Bill"}, :post_script => ["Remove Duplicates", 10])
|
55
|
+
#
|
56
|
+
# This sample runs the script called "Remove Duplicates" and passes it the value +10+ as its
|
57
|
+
# script parameter.
|
58
|
+
#
|
59
|
+
# =Common Options
|
60
|
+
#
|
61
|
+
# Most of the methods on the Layout object accept an optional hash of +options+ to manipulate the
|
62
|
+
# action. For example, when you perform a find, you will typiclaly get back _all_ matching records.
|
63
|
+
# If you want to limit the number of records returned, you can do this:
|
64
|
+
#
|
65
|
+
# myLayout.find({"First Name" => "Bill"}, :max_records => 100)
|
66
|
+
#
|
67
|
+
# The +:max_records+ option tells FileMaker to limit the number of records returned.
|
68
|
+
#
|
69
|
+
# This is the complete list of available options:
|
70
|
+
#
|
71
|
+
# * *max_records* tells FileMaker how many records to return
|
72
|
+
#
|
73
|
+
# * *skip_records* tells FileMaker how many records in the found set to skip, before
|
74
|
+
# returning results; this is typically combined with +max_records+ to "page" through
|
75
|
+
# records
|
76
|
+
#
|
77
|
+
# * *sort_field* tells FileMaker to sort the records by the specified field
|
78
|
+
#
|
79
|
+
# * *sort_order* can be +descend+ or +ascend+ and determines the order
|
80
|
+
# of the sort when +sort_field+ is specified
|
81
|
+
#
|
82
|
+
# * *post_script* tells FileMaker to perform a script after carrying out the action; you
|
83
|
+
# can pass the script name, or a two-element array, with the script name first, then the
|
84
|
+
# script parameter
|
85
|
+
#
|
86
|
+
# * *pre_find_script* is like +post_script+ except the script runs before any find is
|
87
|
+
# performed
|
88
|
+
#
|
89
|
+
# * *pre_sort_script* is like +pre_find_script+ except the script runs after any find
|
90
|
+
# and before any sort
|
91
|
+
#
|
92
|
+
# * *response_layout* tells FileMaker to switch layouts before producing the response; this
|
93
|
+
# is useful when you need a field on a layout to perform a find, edit, or create, but you
|
94
|
+
# want to improve performance by not including the field in the result
|
95
|
+
#
|
96
|
+
# * *logical_operator* can be +and+ or +or+ and tells FileMaker how to process multiple fields
|
97
|
+
# in a find request
|
98
|
+
#
|
99
|
+
# * *modification_id* lets you pass in the modification id from a Record object with the request;
|
100
|
+
# when you do, the action will fail if the record was modified in FileMaker after it was retrieved
|
101
|
+
# by RFM but before the action was run
|
102
|
+
#
|
103
|
+
#
|
104
|
+
# =Attributes
|
105
|
+
#
|
106
|
+
# The Layout object has a few useful attributes:
|
107
|
+
#
|
108
|
+
# * +name+ is the name of the layout
|
109
|
+
#
|
110
|
+
# * +field_controls+ is a hash of FieldControl objects, with the field names as keys. FieldControl's
|
111
|
+
# tell you about the field on the layout: how is it formatted and what value list is assigned
|
112
|
+
#
|
113
|
+
# Note: It is possible to put the same field on a layout more than once. When this is the case, the
|
114
|
+
# value in +field_controls+ for that field is an array with one element representing each instance
|
115
|
+
# of the field.
|
116
|
+
#
|
117
|
+
# * +value_lists+ is a hash of arrays. The keys are value list names, and the values in the hash
|
118
|
+
# are arrays containing the actual value list items. +value_lists+ will include every value
|
119
|
+
# list that is attached to any field on the layout
|
120
|
+
|
121
|
+
class Layout
|
122
|
+
|
123
|
+
# Initialize a layout object. You never really need to do this. Instead, just do this:
|
124
|
+
#
|
125
|
+
# myServer = Rfm::Server.new(...)
|
126
|
+
# myDatabase = myServer["Customers"]
|
127
|
+
# myLayout = myDatabase["Details"]
|
128
|
+
#
|
129
|
+
# This sample code gets a layout object representing the Details layout in the Customers database
|
130
|
+
# on the FileMaker server.
|
131
|
+
#
|
132
|
+
# In case it isn't obvious, this is more easily expressed this way:
|
133
|
+
#
|
134
|
+
# myServer = Rfm::Server.new(...)
|
135
|
+
# myLayout = myServer["Customers"]["Details"]
|
136
|
+
def initialize(name, db)
|
137
|
+
@name = name
|
138
|
+
@db = db
|
139
|
+
|
140
|
+
@loaded = false
|
141
|
+
@field_controls = Rfm::CaseInsensitiveHash.new
|
142
|
+
@value_lists = Rfm::CaseInsensitiveHash.new
|
143
|
+
end
|
144
|
+
|
145
|
+
attr_reader :name, :db
|
146
|
+
|
147
|
+
# Returns a ResultSet object containing _every record_ in the table associated with this layout.
|
148
|
+
def all(options = {})
|
149
|
+
get_records('-findall', {}, options)
|
150
|
+
end
|
151
|
+
|
152
|
+
# Returns a ResultSet containing a single random record from the table associated with this layout.
|
153
|
+
def any(options = {})
|
154
|
+
get_records('-findany', {}, options)
|
155
|
+
end
|
156
|
+
|
157
|
+
# Finds a record. Typically you will pass in a hash of field names and values. For example:
|
158
|
+
#
|
159
|
+
# myLayout.find({"First Name" => "Bill"})
|
160
|
+
#
|
161
|
+
# Values in the hash work just like value in FileMaker's Find mode. You can use any special
|
162
|
+
# symbols (+==+, +...+, +>+, etc...).
|
163
|
+
#
|
164
|
+
# If you pass anything other than a hash as the first parameter, it is converted to a string and
|
165
|
+
# assumed to be FileMaker's internal id for a record (the recid).
|
166
|
+
def find(hash_or_recid, options = {})
|
167
|
+
if hash_or_recid.kind_of? Hash
|
168
|
+
get_records('-find', hash_or_recid, options)
|
169
|
+
else
|
170
|
+
get_records('-find', {'-recid' => hash_or_recid.to_s}, options)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
# Updates the contents of the record whose internal +recid+ is specified. Send in a hash of new
|
175
|
+
# data in the +values+ parameter. Returns a RecordSet containing the modified record. For example:
|
176
|
+
#
|
177
|
+
# recid = myLayout.find({"First Name" => "Bill"})[0].record_id
|
178
|
+
# myLayout.edit(recid, {"First Name" => "Steve"})
|
179
|
+
#
|
180
|
+
# The above code would find the first record with _Bill_ in the First Name field and change the
|
181
|
+
# first name to _Steve_.
|
182
|
+
def edit(recid, values, options = {})
|
183
|
+
get_records('-edit', {'-recid' => recid}.merge(values), options)
|
184
|
+
end
|
185
|
+
|
186
|
+
# Creates a new record in the table associated with this layout. Pass field data as a hash in the
|
187
|
+
# +values+ parameter. Returns the newly created record in a RecordSet. You can use the returned
|
188
|
+
# record to, ie, discover the values in auto-enter fields (like serial numbers).
|
189
|
+
#
|
190
|
+
# For example:
|
191
|
+
#
|
192
|
+
# result = myLayout.create({"First Name" => "Jerry", "Last Name" => "Robin"})
|
193
|
+
# id = result[0]["ID"]
|
194
|
+
#
|
195
|
+
# The above code adds a new record with first name _Jerry_ and last name _Robin_. It then
|
196
|
+
# puts the value from the ID field (a serial number) into a ruby variable called +id+.
|
197
|
+
def create(values, options = {})
|
198
|
+
get_records('-new', values, options)
|
199
|
+
end
|
200
|
+
|
201
|
+
# Deletes the record with the specified internal recid. Returns a ResultSet with the deleted record.
|
202
|
+
#
|
203
|
+
# For example:
|
204
|
+
#
|
205
|
+
# recid = myLayout.find({"First Name" => "Bill"})[0].record_id
|
206
|
+
# myLayout.delete(recid)
|
207
|
+
#
|
208
|
+
# The above code finds every record with _Bill_ in the First Name field, then deletes the first one.
|
209
|
+
def delete(recid, options = {})
|
210
|
+
get_records('-delete', {'-recid' => recid}, options)
|
211
|
+
return nil
|
212
|
+
end
|
213
|
+
|
214
|
+
def field_controls
|
215
|
+
load unless @loaded
|
216
|
+
@field_controls
|
217
|
+
end
|
218
|
+
|
219
|
+
def value_lists
|
220
|
+
load unless @loaded
|
221
|
+
@value_lists
|
222
|
+
end
|
223
|
+
|
224
|
+
private
|
225
|
+
|
226
|
+
def load
|
227
|
+
#require 'rexml/document'
|
228
|
+
require 'nokogiri'
|
229
|
+
|
230
|
+
@loaded = true
|
231
|
+
#fmpxmllayout = @db.server.load_layout(self).body
|
232
|
+
fmpxmllayout = @db.server.load_layout(self)
|
233
|
+
#doc = REXML::Document.new(fmpxmllayout)
|
234
|
+
doc = Nokogiri::XML(fmpxmllayout)
|
235
|
+
#root = doc.root
|
236
|
+
|
237
|
+
# check for errors
|
238
|
+
#error = root.elements['ERRORCODE'].text.to_i
|
239
|
+
error = doc.xpath('//ERRORCODE').children[0].to_s.to_i
|
240
|
+
raise Rfm::Error::FileMakerError.getError(error) if error != 0
|
241
|
+
|
242
|
+
# process valuelists
|
243
|
+
if doc.xpath('//VALUELIST').size > 0 #root.elements['VALUELISTS'].size > 0
|
244
|
+
#root.elements['VALUELISTS'].each_element('VALUELIST') { |valuelist|
|
245
|
+
doc.xpath('//VALUELIST').each {|valuelist|
|
246
|
+
name = valuelist['NAME']
|
247
|
+
#@value_lists[name] = valuelist.elements.collect {|e| e.text}
|
248
|
+
@value_lists[name] = valuelist.children.collect{|value|
|
249
|
+
Rfm::Metadata::ValueListItem.new(value.children[0].to_s, value['DISPLAY'], name)
|
250
|
+
}
|
251
|
+
}
|
252
|
+
@value_lists.freeze
|
253
|
+
end
|
254
|
+
|
255
|
+
# process field controls
|
256
|
+
#root.elements['LAYOUT'].each_element('FIELD') { |field|
|
257
|
+
doc.xpath('//FIELD').each {|field|
|
258
|
+
#name = field.attributes['NAME']
|
259
|
+
name = field['NAME']
|
260
|
+
style_xml = field.children[0]
|
261
|
+
#style = field.elements['STYLE'].attributes['TYPE']
|
262
|
+
style = style_xml['TYPE']
|
263
|
+
#value_list_name = field.elements['STYLE'].attributes['VALUELIST']
|
264
|
+
value_list_name = style_xml['VALUELIST']
|
265
|
+
value_list = @value_lists[value_list_name] if value_list_name != ''
|
266
|
+
field_control = Rfm::Metadata::FieldControl.new(name, style, value_list_name, value_list)
|
267
|
+
existing = @field_controls[name]
|
268
|
+
if existing
|
269
|
+
if existing.kind_of?(Array)
|
270
|
+
existing << field_control
|
271
|
+
else
|
272
|
+
@field_controls[name] = Array[existing, field_control]
|
273
|
+
end
|
274
|
+
else
|
275
|
+
@field_controls[name] = field_control
|
276
|
+
end
|
277
|
+
}
|
278
|
+
@field_controls.freeze
|
279
|
+
end
|
280
|
+
|
281
|
+
def get_records(action, extra_params = {}, options = {})
|
282
|
+
include_portals = options[:include_portals] ? options.delete(:include_portals) : nil
|
283
|
+
xml_response = @db.server.connect(@db.account_name, @db.password, action, params.merge(extra_params), options).body
|
284
|
+
Rfm::Resultset.new(@db.server, xml_response, self, include_portals)
|
285
|
+
end
|
286
|
+
|
287
|
+
def params
|
288
|
+
{"-db" => @db.name, "-lay" => self.name}
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|