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