ginjo-rfm 1.4.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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