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