ginjo-rfm 1.4.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -0,0 +1,93 @@
|
|
1
|
+
module Rfm
|
2
|
+
module Metadata
|
3
|
+
# The Field object represents a single FileMaker field. It *does not hold the data* in the field. Instead,
|
4
|
+
# it serves as a source of metadata about the field. For example, if you're script is trying to be highly
|
5
|
+
# dynamic about its field access, it may need to determine the data type of a field at run time. Here's
|
6
|
+
# how:
|
7
|
+
#
|
8
|
+
# field_name = "Some Field Name"
|
9
|
+
# case myRecord.fields[field_name].result
|
10
|
+
# when "text"
|
11
|
+
# # it is a text field, so handle appropriately
|
12
|
+
# when "number"
|
13
|
+
# # it is a number field, so handle appropriately
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# =Attributes
|
17
|
+
#
|
18
|
+
# The Field object has the following attributes:
|
19
|
+
#
|
20
|
+
# * *name* is the name of the field
|
21
|
+
#
|
22
|
+
# * *result* is the data type of the field; possible values include:
|
23
|
+
# * text
|
24
|
+
# * number
|
25
|
+
# * date
|
26
|
+
# * time
|
27
|
+
# * timestamp
|
28
|
+
# * container
|
29
|
+
#
|
30
|
+
# * *type* any of these:
|
31
|
+
# * normal (a normal data field)
|
32
|
+
# * calculation
|
33
|
+
# * summary
|
34
|
+
#
|
35
|
+
# * *max_repeats* is the number of repetitions (1 for a normal field, more for a repeating field)
|
36
|
+
#
|
37
|
+
# * *global* is +true+ is this is a global field, *false* otherwise
|
38
|
+
#
|
39
|
+
# Note: Field types match FileMaker's own values, but the terminology differs. The +result+ attribute
|
40
|
+
# tells you the data type of the field, regardless of whether it is a calculation, summary, or normal
|
41
|
+
# field. So a calculation field whose result type is _timestamp_ would have these attributes:
|
42
|
+
#
|
43
|
+
# * result: timestamp
|
44
|
+
# * type: calculation
|
45
|
+
#
|
46
|
+
# * *control& is a FieldControl object representing the sytle and value list information associated
|
47
|
+
# with this field on the layout.
|
48
|
+
#
|
49
|
+
# Note: Since a field can sometimes appear on a layout more than once, +control+ may be an Array.
|
50
|
+
# If you don't know ahead of time, you'll need to deal with this. One easy way is:
|
51
|
+
#
|
52
|
+
# controls = [myField.control].flatten
|
53
|
+
# controls.each {|control|
|
54
|
+
# # do something with the control here
|
55
|
+
# }
|
56
|
+
#
|
57
|
+
# The code above makes sure the control is always an array. Typically, though, you'll know up front
|
58
|
+
# if the control is an array or not, and you can code accordingly.
|
59
|
+
|
60
|
+
class Field
|
61
|
+
|
62
|
+
attr_reader :name, :result, :type, :max_repeats, :global
|
63
|
+
|
64
|
+
# Initializes a field object. You'll never need to do this. Instead, get your Field objects from
|
65
|
+
# ResultSet::fields
|
66
|
+
def initialize(field)
|
67
|
+
@name = field['name']
|
68
|
+
@result = field['result']
|
69
|
+
@type = field['type']
|
70
|
+
@max_repeats = field['max-repeats']
|
71
|
+
@global = field['global']
|
72
|
+
end
|
73
|
+
|
74
|
+
# Coerces the text value from an +fmresultset+ document into proper Ruby types based on the
|
75
|
+
# type of the field. You'll never need to do this: Rfm does it automatically for you when you
|
76
|
+
# access field data through the Record object.
|
77
|
+
def coerce(value, resultset)
|
78
|
+
return nil if value.empty?
|
79
|
+
case self.result
|
80
|
+
when "text" then value
|
81
|
+
when "number" then BigDecimal.new(value)
|
82
|
+
when "date" then Date.strptime(value, resultset.date_format)
|
83
|
+
when "time" then DateTime.strptime("1/1/-4712 #{value}", "%m/%d/%Y #{resultset.time_format}")
|
84
|
+
when "timestamp" then DateTime.strptime(value, resultset.timestamp_format)
|
85
|
+
when "container" then URI.parse("#{resultset.server.scheme}://#{resultset.server.host_name}:#{resultset.server.port}#{value}")
|
86
|
+
else nil
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Rfm
|
2
|
+
module Metadata
|
3
|
+
|
4
|
+
# The FieldControl object represents a field on a FileMaker layout. You can find out what field
|
5
|
+
# style the field uses, and the value list attached to it.
|
6
|
+
#
|
7
|
+
# =Attributes
|
8
|
+
#
|
9
|
+
# * *name* is the name of the field
|
10
|
+
#
|
11
|
+
# * *style* is any one of:
|
12
|
+
# * * :edit_box - a normal editable field
|
13
|
+
# * * :scrollable - an editable field with scroll bar
|
14
|
+
# * * :popup_menu - a pop-up menu
|
15
|
+
# * * :checkbox_set - a set of checkboxes
|
16
|
+
# * * :radio_button_set - a set of radio buttons
|
17
|
+
# * * :popup_list - a pop-up list
|
18
|
+
# * * :calendar - a pop-up calendar
|
19
|
+
#
|
20
|
+
# * *value_list_name* is the name of the attached value list, if any
|
21
|
+
#
|
22
|
+
# * *value_list* is an array of strings representing the value list items, or nil
|
23
|
+
# if this field has no attached value list
|
24
|
+
class FieldControl
|
25
|
+
def initialize(name, style, value_list_name, value_list)
|
26
|
+
@name = name
|
27
|
+
case style
|
28
|
+
when "EDITTEXT"
|
29
|
+
@style = :edit_box
|
30
|
+
when "POPUPMENU"
|
31
|
+
@style = :popup_menu
|
32
|
+
when "CHECKBOX"
|
33
|
+
@style = :checkbox_set
|
34
|
+
when "RADIOBUTTONS"
|
35
|
+
@style = :radio_button_set
|
36
|
+
when "POPUPLIST"
|
37
|
+
@style = :popup_list
|
38
|
+
when "CALENDAR"
|
39
|
+
@style = :calendar
|
40
|
+
when "SCROLLTEXT"
|
41
|
+
@style = :scrollable
|
42
|
+
else
|
43
|
+
nil
|
44
|
+
end
|
45
|
+
@value_list_name = value_list_name
|
46
|
+
@value_list = value_list
|
47
|
+
end
|
48
|
+
|
49
|
+
attr_reader :name, :style, :value_list_name, :value_list
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Rfm
|
2
|
+
module Metadata
|
3
|
+
# The Script object represents a FileMaker script. At this point, the Script object exists only so
|
4
|
+
# you can enumrate all scripts in a Database (which is a rare need):
|
5
|
+
#
|
6
|
+
# myDatabase.script.each {|script|
|
7
|
+
# puts script.name
|
8
|
+
# }
|
9
|
+
#
|
10
|
+
# If you want to _run_ a script, see the Layout object instead.
|
11
|
+
class Script
|
12
|
+
def initialize(name, db)
|
13
|
+
@name = name
|
14
|
+
@db = db
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :name
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Rfm
|
2
|
+
module Metadata
|
3
|
+
|
4
|
+
# The ValueListItem object represents a item in a Filemaker value list.
|
5
|
+
#
|
6
|
+
# =Attributes
|
7
|
+
#
|
8
|
+
# * *value* the value list item value
|
9
|
+
#
|
10
|
+
# * *display* is the value list item display string
|
11
|
+
# * * it could be the same as value, or it could be the "second field" # * * :scrollable - an editable field with scroll bar
|
12
|
+
# * * if that option is checked in Filemaker
|
13
|
+
#
|
14
|
+
# * *value_list_name* is the name of the parent value list, if any
|
15
|
+
class ValueListItem < String
|
16
|
+
def initialize(value, display, value_list_name)
|
17
|
+
@value_list_name = value_list_name
|
18
|
+
@value = value
|
19
|
+
@display = display
|
20
|
+
self.replace value
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_reader :value, :display, :value_list_name
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/rfm/record.rb
ADDED
@@ -0,0 +1,232 @@
|
|
1
|
+
module Rfm
|
2
|
+
|
3
|
+
# The Record object represents a single FileMaker record. You typically get them from ResultSet objects.
|
4
|
+
# For example, you might use a Layout object to find some records:
|
5
|
+
#
|
6
|
+
# results = myLayout.find({"First Name" => "Bill"})
|
7
|
+
#
|
8
|
+
# The +results+ variable in this example now contains a ResultSet object. ResultSets are really just arrays of
|
9
|
+
# Record objects (with a little extra added in). So you can get a record object just like you would access any
|
10
|
+
# typical array element:
|
11
|
+
#
|
12
|
+
# first_record = results[0]
|
13
|
+
#
|
14
|
+
# You can find out how many record were returned:
|
15
|
+
#
|
16
|
+
# record_count = results.size
|
17
|
+
#
|
18
|
+
# And you can of course iterate:
|
19
|
+
#
|
20
|
+
# results.each (|record|
|
21
|
+
# // you can work with the record here
|
22
|
+
# )
|
23
|
+
#
|
24
|
+
# =Accessing Field Data
|
25
|
+
#
|
26
|
+
# You can access field data in the Record object in two ways. Typically, you simply treat Record like a hash
|
27
|
+
# (because it _is_ a hash...I love OOP). Keys are field names:
|
28
|
+
#
|
29
|
+
# first = myRecord["First Name"]
|
30
|
+
# last = myRecord["Last Name"]
|
31
|
+
#
|
32
|
+
# If your field naming conventions mean that your field names are also valid Ruby symbol named (ie: they contain only
|
33
|
+
# letters, numbers, and underscores) then you can treat them like attributes of the record. For example, if your fields
|
34
|
+
# are called "first_name" and "last_name" you can do this:
|
35
|
+
#
|
36
|
+
# first = myRecord.first_name
|
37
|
+
# last = myRecord.last_name
|
38
|
+
#
|
39
|
+
# Note: This shortcut will fail (in a rather mysterious way) if your field name happens to match any real attribute
|
40
|
+
# name of a Record object. For instance, you may have a field called "server". If you try this:
|
41
|
+
#
|
42
|
+
# server_name = myRecord.server
|
43
|
+
#
|
44
|
+
# you'll actually set +server_name+ to the Rfm::Server object this Record came from. This won't fail until you try
|
45
|
+
# to treat it as a String somewhere else in your code. It is also possible a future version of Rfm will include
|
46
|
+
# new attributes on the Record class which may clash with your field names. This will cause perfectly valid code
|
47
|
+
# today to fail later when you upgrade. If you can't stomach this kind of insanity, stick with the hash-like
|
48
|
+
# method of field access, which has none of these limitations. Also note that the +myRecord[]+ method is probably
|
49
|
+
# somewhat faster since it doesn't go through +method_missing+.
|
50
|
+
#
|
51
|
+
# =Accessing Repeating Fields
|
52
|
+
#
|
53
|
+
# If you have a repeating field, RFM simply returns an array:
|
54
|
+
#
|
55
|
+
# val1 = myRecord["Price"][0]
|
56
|
+
# val2 = myRecord["Price"][1]
|
57
|
+
#
|
58
|
+
# In the above example, the Price field is a repeating field. The code puts the first repetition in a variable called
|
59
|
+
# +val1+ and the second in a variable called +val2+.
|
60
|
+
#
|
61
|
+
# =Accessing Portals
|
62
|
+
#
|
63
|
+
# If the ResultSet includes portals (because the layout it comes from has portals on it) you can access them
|
64
|
+
# using the Record::portals attribute. It is a hash with table occurrence names for keys, and arrays of Record
|
65
|
+
# objects for values. In other words, you can do this:
|
66
|
+
#
|
67
|
+
# myRecord.portals["Orders"].each {|record|
|
68
|
+
# puts record["Order Number"]
|
69
|
+
# }
|
70
|
+
#
|
71
|
+
# This code iterates through the rows of the _Orders_ portal.
|
72
|
+
#
|
73
|
+
# =Field Types and Ruby Types
|
74
|
+
#
|
75
|
+
# RFM automatically converts data from FileMaker into a Ruby object with the most reasonable type possible. The
|
76
|
+
# type are mapped thusly:
|
77
|
+
#
|
78
|
+
# * *Text* fields are converted to Ruby String objects
|
79
|
+
#
|
80
|
+
# * *Number* fields are converted to Ruby BigDecimal objects (the basic Ruby numeric types have
|
81
|
+
# much less precision and range than FileMaker number fields)
|
82
|
+
#
|
83
|
+
# * *Date* fields are converted to Ruby Date objects
|
84
|
+
#
|
85
|
+
# * *Time* fields are converted to Ruby DateTime objects (you can ignore the date component)
|
86
|
+
#
|
87
|
+
# * *Timestamp* fields are converted to Ruby DateTime objects
|
88
|
+
#
|
89
|
+
# * *Container* fields are converted to Ruby URI objects
|
90
|
+
#
|
91
|
+
# =Attributes
|
92
|
+
#
|
93
|
+
# In addition to +portals+, the Record object has these useful attributes:
|
94
|
+
#
|
95
|
+
# * *record_id* is FileMaker's internal identifier for this record (_not_ any ID field you might have
|
96
|
+
# in your table); you need a +record_id+ to edit or delete a record
|
97
|
+
#
|
98
|
+
# * *mod_id* is the modification identifier for the record; whenever a record is modified, its +mod_id+
|
99
|
+
# changes so you can tell if the Record object you're looking at is up-to-date as compared to another
|
100
|
+
# copy of the same record
|
101
|
+
class Record < Rfm::CaseInsensitiveHash
|
102
|
+
|
103
|
+
attr_reader :record_id, :mod_id, :portals
|
104
|
+
|
105
|
+
def initialize(record, result, field_meta, layout, portal=nil)
|
106
|
+
@record_id = record['record-id']
|
107
|
+
@mod_id = record['mod-id']
|
108
|
+
@mods = {}
|
109
|
+
@layout = layout
|
110
|
+
@portals ||= Rfm::CaseInsensitiveHash.new
|
111
|
+
|
112
|
+
relatedsets = !portal && result.instance_variable_get(:@include_portals) ? record.xpath('relatedset') : []
|
113
|
+
|
114
|
+
record.xpath('field').each do |field|
|
115
|
+
field_name = field['name']
|
116
|
+
field_name.gsub!(Regexp.new(portal + '::'), '') if portal
|
117
|
+
datum = []
|
118
|
+
|
119
|
+
field.xpath('data').each do |x|
|
120
|
+
datum.push(field_meta[field_name].coerce(x.inner_text, result))
|
121
|
+
end
|
122
|
+
|
123
|
+
if datum.length == 1
|
124
|
+
self[field_name] = datum[0]
|
125
|
+
elsif datum.length == 0
|
126
|
+
self[field_name] = nil
|
127
|
+
else
|
128
|
+
self[field_name] = datum
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
unless relatedsets.empty?
|
133
|
+
relatedsets.each do |relatedset|
|
134
|
+
tablename, records = relatedset['table'], []
|
135
|
+
|
136
|
+
relatedset.xpath('record').each do |record|
|
137
|
+
records << self.class.new(record, result, result.portal_meta[tablename], layout, tablename)
|
138
|
+
end
|
139
|
+
|
140
|
+
@portals[tablename] = records
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
@loaded = true
|
145
|
+
end
|
146
|
+
|
147
|
+
def self.build_records(records, result, field_meta, layout, portal=nil)
|
148
|
+
records.each do |record|
|
149
|
+
result << self.new(record, result, field_meta, layout, portal)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# Saves local changes to the Record object back to Filemaker. For example:
|
154
|
+
#
|
155
|
+
# myLayout.find({"First Name" => "Bill"}).each(|record|
|
156
|
+
# record["First Name"] = "Steve"
|
157
|
+
# record.save
|
158
|
+
# )
|
159
|
+
#
|
160
|
+
# This code finds every record with _Bill_ in the First Name field, then changes the first name to
|
161
|
+
# Steve.
|
162
|
+
#
|
163
|
+
# Note: This method is smart enough to not bother saving if nothing has changed. So there's no need
|
164
|
+
# to optimize on your end. Just save, and if you've changed the record it will be saved. If not, no
|
165
|
+
# server hit is incurred.
|
166
|
+
def save
|
167
|
+
self.merge!(@layout.edit(self.record_id, @mods)[0]) if @mods.size > 0
|
168
|
+
@mods.clear
|
169
|
+
end
|
170
|
+
|
171
|
+
# Like Record::save, except it fails (and raises an error) if the underlying record in FileMaker was
|
172
|
+
# modified after the record was fetched but before it was saved. In other words, prevents you from
|
173
|
+
# accidentally overwriting changes someone else made to the record.
|
174
|
+
def save_if_not_modified
|
175
|
+
self.merge!(@layout.edit(@record_id, @mods, {:modification_id => @mod_id})[0]) if @mods.size > 0
|
176
|
+
@mods.clear
|
177
|
+
end
|
178
|
+
|
179
|
+
# Gets the value of a field from the record. For example:
|
180
|
+
#
|
181
|
+
# first = myRecord["First Name"]
|
182
|
+
# last = myRecord["Last Name"]
|
183
|
+
#
|
184
|
+
# This sample puts the first and last name from the record into Ruby variables.
|
185
|
+
#
|
186
|
+
# You can also update a field:
|
187
|
+
#
|
188
|
+
# myRecord["First Name"] = "Sophia"
|
189
|
+
#
|
190
|
+
# When you do, the change is noted, but *the data is not updated in FileMaker*. You must call
|
191
|
+
# Record::save or Record::save_if_not_modified to actually save the data.
|
192
|
+
def []=(name, value)
|
193
|
+
name = name.to_s
|
194
|
+
return super unless @loaded
|
195
|
+
raise Rfm::ParameterError,
|
196
|
+
"You attempted to modify a field that does not exist in the current Filemaker layout." unless self.key?(name)
|
197
|
+
@mods[name] = value
|
198
|
+
self.merge! @mods
|
199
|
+
end
|
200
|
+
|
201
|
+
alias :_old_hash_reader :[]
|
202
|
+
def [](value)
|
203
|
+
read_attribute(value)
|
204
|
+
end
|
205
|
+
|
206
|
+
def respond_to?(symbol, include_private = false)
|
207
|
+
return true if self.include?(symbol.to_s)
|
208
|
+
super
|
209
|
+
end
|
210
|
+
|
211
|
+
private
|
212
|
+
|
213
|
+
def read_attribute(key)
|
214
|
+
key = key.to_s
|
215
|
+
raise NoMethodError,
|
216
|
+
"#{key.to_s} does not exists as a field in the current Filemaker layout." unless (!@layout or self.key?(key))
|
217
|
+
self._old_hash_reader(key).to_s.empty? ? nil : self._old_hash_reader(key) if self._old_hash_reader(key)
|
218
|
+
end
|
219
|
+
|
220
|
+
def method_missing (symbol, *attrs, &block)
|
221
|
+
method = symbol.to_s
|
222
|
+
return read_attribute(method) if self.key?(method)
|
223
|
+
|
224
|
+
if method =~ /(=)$/ && self.key?($`)
|
225
|
+
#return @mods[$`] = attrs.first
|
226
|
+
return self[$`] = attrs.first
|
227
|
+
end
|
228
|
+
super
|
229
|
+
end
|
230
|
+
|
231
|
+
end
|
232
|
+
end
|
@@ -0,0 +1,137 @@
|
|
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
|
8
|
+
require 'nokogiri'
|
9
|
+
require 'bigdecimal'
|
10
|
+
require 'rfm/record'
|
11
|
+
#require 'rfm/metadata/field'
|
12
|
+
|
13
|
+
module Rfm
|
14
|
+
|
15
|
+
# The ResultSet object represents a set of records in FileMaker. It is, in every way, a real Ruby
|
16
|
+
# Array, so everything you expect to be able to do with an Array can be done with a ResultSet as well.
|
17
|
+
# In this case, the elements in the array are Record objects.
|
18
|
+
#
|
19
|
+
# Here's a typical example, displaying the results of a Find:
|
20
|
+
#
|
21
|
+
# myServer = Rfm::Server.new(...)
|
22
|
+
# results = myServer["Customers"]["Details"].find("First Name" => "Bill")
|
23
|
+
# results.each {|record|
|
24
|
+
# puts record["First Name"]
|
25
|
+
# puts record["Last Name"]
|
26
|
+
# puts record["Email Address"]
|
27
|
+
# }
|
28
|
+
#
|
29
|
+
# =Attributes
|
30
|
+
#
|
31
|
+
# The ResultSet object has these attributes:
|
32
|
+
#
|
33
|
+
# * *field_meta* is a hash with field names for keys and Field objects for values; it provides
|
34
|
+
# info about the fields in the ResultSet
|
35
|
+
#
|
36
|
+
# * *portal_meta* 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
|
+
|
39
|
+
class Resultset < Array
|
40
|
+
|
41
|
+
attr_reader :layout, :server
|
42
|
+
attr_reader :field_meta, :portal_meta
|
43
|
+
attr_reader :date_format, :time_format, :timestamp_format
|
44
|
+
attr_reader :total_count, :foundset_count
|
45
|
+
|
46
|
+
# Initializes a new ResultSet object. You will probably never do this your self (instead, use the Layout
|
47
|
+
# object to get various ResultSet obejects).
|
48
|
+
#
|
49
|
+
# If you feel so inclined, though, pass a Server object, and some +fmpxmlresult+ compliant XML in a String.
|
50
|
+
#
|
51
|
+
# =Attributes
|
52
|
+
#
|
53
|
+
# The ResultSet object includes several useful attributes:
|
54
|
+
#
|
55
|
+
# * *fields* is a hash (with field names for keys and Field objects for values). It includes an entry for
|
56
|
+
# every field in the ResultSet. Note: You don't use Field objects to access _data_. If you're after
|
57
|
+
# data, get a Record object (ResultSet is an array of records). Field objects tell you about the fields
|
58
|
+
# (their type, repetitions, and so forth) in case you find that information useful programmatically.
|
59
|
+
#
|
60
|
+
# Note: keys in the +fields+ hash are downcased for convenience (and [] automatically downcases on
|
61
|
+
# lookup, so it should be seamless). But if you +each+ a field hash and need to know a field's real
|
62
|
+
# name, with correct case, do +myField.name+ instead of relying on the key in the hash.
|
63
|
+
#
|
64
|
+
# * *portals* is a hash (with table occurrence names for keys and Field objects for values). If your
|
65
|
+
# layout contains portals, you can find out what fields they contain here. Again, if it's the data you're
|
66
|
+
# after, you want to look at the Record object.
|
67
|
+
|
68
|
+
def initialize(server, xml_response, layout, portals=nil)
|
69
|
+
@layout = layout
|
70
|
+
@server = server
|
71
|
+
@field_meta ||= Rfm::CaseInsensitiveHash.new
|
72
|
+
@portal_meta ||= Rfm::CaseInsensitiveHash.new
|
73
|
+
@include_portals = portals
|
74
|
+
|
75
|
+
doc = Nokogiri.XML(remove_namespace(xml_response))
|
76
|
+
|
77
|
+
error = doc.xpath('/fmresultset/error').attribute('code').value.to_i
|
78
|
+
check_for_errors(error, server.state[:raise_on_401])
|
79
|
+
|
80
|
+
datasource = doc.xpath('/fmresultset/datasource')
|
81
|
+
meta = doc.xpath('/fmresultset/metadata')
|
82
|
+
resultset = doc.xpath('/fmresultset/resultset')
|
83
|
+
|
84
|
+
@date_format = convert_date_time_format(datasource.attribute('date-format').value)
|
85
|
+
@time_format = convert_date_time_format(datasource.attribute('time-format').value)
|
86
|
+
@timestamp_format = convert_date_time_format(datasource.attribute('timestamp-format').value)
|
87
|
+
|
88
|
+
@foundset_count = resultset.attribute('count').value.to_i
|
89
|
+
@total_count = datasource.attribute('total-count').value.to_i
|
90
|
+
|
91
|
+
parse_fields(meta)
|
92
|
+
parse_portals(meta) if @include_portals
|
93
|
+
|
94
|
+
Rfm::Record.build_records(resultset.xpath('record'), self, @field_meta, @layout)
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
def remove_namespace(xml)
|
100
|
+
xml.gsub('xmlns="http://www.filemaker.com/xml/fmresultset" version="1.0"', '')
|
101
|
+
end
|
102
|
+
|
103
|
+
def check_for_errors(code, raise_401)
|
104
|
+
raise Rfm::Error.getError(code) if code != 0 && (code != 401 || raise_401)
|
105
|
+
end
|
106
|
+
|
107
|
+
def parse_fields(meta)
|
108
|
+
meta.xpath('field-definition').each do |field|
|
109
|
+
@field_meta[field['name']] = Rfm::Metadata::Field.new(field)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def parse_portals(meta)
|
114
|
+
meta.xpath('relatedset-definition').each do |relatedset|
|
115
|
+
table, fields = relatedset.attribute('table').value, {}
|
116
|
+
|
117
|
+
relatedset.xpath('field-definition').each do |field|
|
118
|
+
name = field.attribute('name').value.gsub(Regexp.new(table + '::'), '')
|
119
|
+
fields[name] = Rfm::Metadata::Field.new(field)
|
120
|
+
end
|
121
|
+
|
122
|
+
@portal_meta[table] = fields
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def convert_date_time_format(fm_format)
|
127
|
+
fm_format.gsub!('MM', '%m')
|
128
|
+
fm_format.gsub!('dd', '%d')
|
129
|
+
fm_format.gsub!('yyyy', '%Y')
|
130
|
+
fm_format.gsub!('HH', '%H')
|
131
|
+
fm_format.gsub!('mm', '%M')
|
132
|
+
fm_format.gsub!('ss', '%S')
|
133
|
+
fm_format
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
end
|