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