ginjo-rfm 1.4.4 → 2.0.pre31

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.
@@ -9,12 +9,14 @@ module Rfm
9
9
  #
10
10
  # If you want to _run_ a script, see the Layout object instead.
11
11
  class Script
12
- def initialize(name, db)
12
+ def initialize(name, db_obj)
13
13
  @name = name
14
- @db = db
14
+ self.db = db_obj
15
15
  end
16
16
 
17
+ meta_attr_accessor :db
17
18
  attr_reader :name
18
- end
19
- end
20
- end
19
+ end # Script
20
+
21
+ end # Metadata
22
+ end # Rfm
@@ -1,5 +1,4 @@
1
1
  module Rfm
2
-
3
2
  # The Record object represents a single FileMaker record. You typically get them from ResultSet objects.
4
3
  # For example, you might use a Layout object to find some records:
5
4
  #
@@ -69,7 +68,14 @@ module Rfm
69
68
  # }
70
69
  #
71
70
  # This code iterates through the rows of the _Orders_ portal.
72
- #
71
+ #
72
+ # As a convenience, you can call a specific portal as a method on your record, if the table occurrence name does
73
+ # not have any characters that are prohibited in ruby method names, just as you can call a field with a method:
74
+ #
75
+ # myRecord.orders.each {|portal_row|
76
+ # puts portal_row["Order Number"]
77
+ # }
78
+ #
73
79
  # =Field Types and Ruby Types
74
80
  #
75
81
  # RFM automatically converts data from FileMaker into a Ruby object with the most reasonable type possible. The
@@ -100,41 +106,51 @@ module Rfm
100
106
  # copy of the same record
101
107
  class Record < Rfm::CaseInsensitiveHash
102
108
 
109
+ attr_accessor :layout, :resultset
103
110
  attr_reader :record_id, :mod_id, :portals
111
+ def_delegators :resultset, :field_meta
112
+ def_delegators :layout, :db, :database, :server
104
113
 
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
114
+ def initialize(record, resultset_obj, field_meta, layout_obj, portal=nil)
111
115
 
112
- relatedsets = !portal && result.instance_variable_get(:@include_portals) ? record.xpath('relatedset') : []
113
-
114
- record.xpath('field').each do |field|
116
+ @layout = layout_obj
117
+ @resultset = resultset_obj
118
+ @record_id = record['record-id']
119
+ @mod_id = record['mod-id']
120
+ @mods = {}
121
+ @portals ||= Rfm::CaseInsensitiveHash.new
122
+
123
+ relatedsets = !portal && resultset_obj.instance_variable_get(:@include_portals) ? record['relatedset'].rfm_force_array : []
124
+
125
+ record['field'].rfm_force_array.each do |field|
126
+ next unless field
115
127
  field_name = field['name']
116
128
  field_name.gsub!(Regexp.new(portal + '::'), '') if portal
117
129
  datum = []
118
130
 
119
- field.xpath('data').each do |x|
120
- datum.push(field_meta[field_name].coerce(x.inner_text, result))
121
- end
131
+ data = field['data']; data = data.is_a?(Hash) ? [data] : data
132
+ data.each do |x|
133
+ next unless field_meta[field_name]
134
+ datum.push(field_meta[field_name].coerce(x['__content__'], resultset_obj))
135
+ end if data
122
136
 
123
137
  if datum.length == 1
124
- self[field_name] = datum[0]
138
+ rfm_super[field_name] = datum[0]
125
139
  elsif datum.length == 0
126
- self[field_name] = nil
140
+ rfm_super[field_name] = nil
127
141
  else
128
- self[field_name] = datum
142
+ rfm_super[field_name] = datum
129
143
  end
130
144
  end
131
145
 
132
146
  unless relatedsets.empty?
133
147
  relatedsets.each do |relatedset|
148
+ next if relatedset.blank?
134
149
  tablename, records = relatedset['table'], []
135
150
 
136
- relatedset.xpath('record').each do |record|
137
- records << self.class.new(record, result, result.portal_meta[tablename], layout, tablename)
151
+ relatedset['record'].rfm_force_array.each do |record|
152
+ next unless record
153
+ records << self.class.new(record, resultset_obj, resultset_obj.portal_meta[tablename], layout_obj, tablename)
138
154
  end
139
155
 
140
156
  @portals[tablename] = records
@@ -144,9 +160,9 @@ module Rfm
144
160
  @loaded = true
145
161
  end
146
162
 
147
- def self.build_records(records, result, field_meta, layout, portal=nil)
163
+ def self.build_records(records, resultset_obj, field_meta, layout_obj, portal=nil)
148
164
  records.each do |record|
149
- result << self.new(record, result, field_meta, layout, portal)
165
+ resultset_obj << self.new(record, resultset_obj, field_meta, layout_obj, portal)
150
166
  end
151
167
  end
152
168
 
@@ -164,16 +180,15 @@ module Rfm
164
180
  # to optimize on your end. Just save, and if you've changed the record it will be saved. If not, no
165
181
  # server hit is incurred.
166
182
  def save
167
- self.merge!(@layout.edit(self.record_id, @mods)[0]) if @mods.size > 0
183
+ self.merge!(layout.edit(self.record_id, @mods)[0]) if @mods.size > 0
168
184
  @mods.clear
169
185
  end
170
-
171
186
 
172
187
  # Like Record::save, except it fails (and raises an error) if the underlying record in FileMaker was
173
188
  # modified after the record was fetched but before it was saved. In other words, prevents you from
174
189
  # accidentally overwriting changes someone else made to the record.
175
190
  def save_if_not_modified
176
- self.merge!(@layout.edit(@record_id, @mods, {:modification_id => @mod_id})[0]) if @mods.size > 0
191
+ self.merge!(layout.edit(@record_id, @mods, {:modification_id => @mod_id})[0]) if @mods.size > 0
177
192
  @mods.clear
178
193
  end
179
194
 
@@ -190,43 +205,43 @@ module Rfm
190
205
  #
191
206
  # When you do, the change is noted, but *the data is not updated in FileMaker*. You must call
192
207
  # Record::save or Record::save_if_not_modified to actually save the data.
193
- def []=(name, value)
194
- name = name.to_s.downcase
195
- return super unless @loaded
196
- raise Rfm::ParameterError,
197
- "You attempted to modify a field that does not exist in the current Filemaker layout." unless self.key?(name)
198
- @mods[name] = value
199
- self.merge! @mods
200
- end
201
-
202
- alias :_old_hash_reader :[]
203
- def [](value)
204
- read_attribute(value)
205
- end
208
+ def [](key)
209
+ return fetch(key.to_s.downcase)
210
+ rescue IndexError
211
+ raise Rfm::ParameterError, "#{key} does not exists as a field in the current Filemaker layout." unless key.to_s == '' #unless (!layout or self.key?(key_string))
212
+ end
206
213
 
207
214
  def respond_to?(symbol, include_private = false)
208
215
  return true if self.include?(symbol.to_s)
209
216
  super
210
217
  end
218
+
219
+
220
+ def []=(key, value)
221
+ key_string = key.to_s.downcase
222
+ return super unless @loaded # is this needed?
223
+ raise Rfm::ParameterError, "You attempted to modify a field that does not exist in the current Filemaker layout." unless self.key?(key_string)
224
+ @mods[key_string] = value
225
+ super(key, value)
226
+ end
227
+
228
+ def field_names
229
+ resultset.field_names rescue layout.field_names
230
+ end
211
231
 
212
- private
213
232
 
214
- def read_attribute(key)
215
- key_string = key.to_s.downcase
216
- raise NoMethodError,
217
- "#{key_string} does not exists as a field in the current Filemaker layout." unless (!@layout or self.key?(key_string)) #!self.keys.grep(/#{key_string}/i).empty?
218
- self._old_hash_reader(key).to_s.empty? ? nil : self._old_hash_reader(key) if self._old_hash_reader(key)
219
- end
233
+ private
220
234
 
221
- def method_missing (symbol, *attrs, &block)
222
- method = symbol.to_s
223
- return read_attribute(method) if self.key?(method)
224
-
225
- if method =~ /(=)$/ && self.key?($`)
226
- return self[$`] = attrs.first
227
- end
228
- super
229
- end
230
-
231
- end
232
- end
235
+ def method_missing (symbol, *attrs, &block)
236
+ method = symbol.to_s
237
+ return self[method] if self.key?(method)
238
+ return @portals[method] if @portals and @portals.key?(method)
239
+
240
+ if method =~ /(=)$/
241
+ return self[$`] = attrs.first if self.key?($`)
242
+ end
243
+ super
244
+ end
245
+
246
+ end # Record
247
+ end # Rfm
@@ -5,7 +5,7 @@
5
5
  # Author:: Geoff Coffey (mailto:gwcoffey@gmail.com)
6
6
  # Copyright:: Copyright (c) 2007 Six Fried Rice, LLC and Mufaddal Khumri
7
7
  # License:: See MIT-LICENSE for details
8
- require 'nokogiri'
8
+
9
9
  require 'bigdecimal'
10
10
  require 'rfm/record'
11
11
 
@@ -40,7 +40,8 @@ module Rfm
40
40
  attr_reader :layout, :server
41
41
  attr_reader :field_meta, :portal_meta
42
42
  attr_reader :date_format, :time_format, :timestamp_format
43
- attr_reader :total_count, :foundset_count
43
+ attr_reader :total_count, :foundset_count, :table
44
+ def_delegators :layout, :db, :database
44
45
 
45
46
  # Initializes a new ResultSet object. You will probably never do this your self (instead, use the Layout
46
47
  # object to get various ResultSet obejects).
@@ -64,62 +65,80 @@ module Rfm
64
65
  # layout contains portals, you can find out what fields they contain here. Again, if it's the data you're
65
66
  # after, you want to look at the Record object.
66
67
 
67
- def initialize(server, xml_response, layout, portals=nil)
68
- @layout = layout
69
- @server = server
68
+ def initialize(server_obj, xml_response, layout_obj, portals=nil)
69
+ @layout = layout_obj
70
+ @server = server_obj
70
71
  @field_meta ||= Rfm::CaseInsensitiveHash.new
71
72
  @portal_meta ||= Rfm::CaseInsensitiveHash.new
72
73
  @include_portals = portals
73
74
 
74
- doc = Nokogiri.XML(remove_namespace(xml_response))
75
+ doc = XmlParser.new(xml_response, :namespace=>false, :parser=>server.state[:parser])
75
76
 
76
- error = doc.xpath('/fmresultset/error').attribute('code').value.to_i
77
+ error = doc['fmresultset']['error']['code'].to_i
77
78
  check_for_errors(error, server.state[:raise_on_401])
78
79
 
79
- datasource = doc.xpath('/fmresultset/datasource')
80
- meta = doc.xpath('/fmresultset/metadata')
81
- resultset = doc.xpath('/fmresultset/resultset')
82
-
83
- @date_format = convert_date_time_format(datasource.attribute('date-format').value)
84
- @time_format = convert_date_time_format(datasource.attribute('time-format').value)
85
- @timestamp_format = convert_date_time_format(datasource.attribute('timestamp-format').value)
80
+ datasource = doc['fmresultset']['datasource']
81
+ meta = doc['fmresultset']['metadata']
82
+ resultset = doc['fmresultset']['resultset']
86
83
 
87
- @foundset_count = resultset.attribute('count').value.to_i
88
- @total_count = datasource.attribute('total-count').value.to_i
84
+ @date_format = convert_date_time_format(datasource['date-format'].to_s)
85
+ @time_format = convert_date_time_format(datasource['time-format'].to_s)
86
+ @timestamp_format = convert_date_time_format(datasource['timestamp-format'].to_s)
89
87
 
88
+ @foundset_count = resultset['count'].to_s.to_i
89
+ @total_count = datasource['total-count'].to_s.to_i
90
+ @table = datasource['table']
91
+
92
+ (layout.table = @table) if layout and layout.table_no_load.blank?
93
+
90
94
  parse_fields(meta)
91
- parse_portals(meta) if @include_portals
92
95
 
93
- Rfm::Record.build_records(resultset.xpath('record'), self, @field_meta, @layout)
96
+ # This will always load portal meta, even if :include_portals was not specified.
97
+ # See Record for control of portal data loading.
98
+ parse_portals(meta)
94
99
 
100
+ return if resultset['record'].nil?
101
+ Rfm::Record.build_records(resultset['record'].rfm_force_array, self, @field_meta, @layout)
95
102
  end
103
+
104
+ def field_names
105
+ field_meta.collect{|k,v| v.name}
106
+ end
107
+
108
+ def portal_names
109
+ portal_meta.keys
110
+ end
111
+
96
112
 
97
113
  private
98
- def remove_namespace(xml)
99
- xml.gsub('xmlns="http://www.filemaker.com/xml/fmresultset" version="1.0"', '')
100
- end
101
114
 
102
115
  def check_for_errors(code, raise_401)
103
116
  raise Rfm::Error.getError(code) if code != 0 && (code != 401 || raise_401)
104
117
  end
105
118
 
106
119
  def parse_fields(meta)
107
- meta.xpath('field-definition').each do |field|
120
+ return if meta['field-definition'].blank?
121
+
122
+ meta['field-definition'].rfm_force_array.each do |field|
108
123
  @field_meta[field['name']] = Rfm::Metadata::Field.new(field)
109
124
  end
125
+ (layout.field_names = field_names) if layout and layout.field_names_no_load.blank?
110
126
  end
111
127
 
112
128
  def parse_portals(meta)
113
- meta.xpath('relatedset-definition').each do |relatedset|
114
- table, fields = relatedset.attribute('table').value, {}
129
+ return if meta['relatedset-definition'].blank?
130
+ meta['relatedset-definition'].rfm_force_array.each do |relatedset|
131
+ next if relatedset.blank?
132
+ table, fields = relatedset['table'], {}
115
133
 
116
- relatedset.xpath('field-definition').each do |field|
117
- name = field.attribute('name').value.gsub(Regexp.new(table + '::'), '')
134
+ relatedset['field-definition'].rfm_force_array.each do |field|
135
+ name = field['name'].to_s.gsub(Regexp.new(table + '::'), '')
118
136
  fields[name] = Rfm::Metadata::Field.new(field)
119
137
  end
120
138
 
121
139
  @portal_meta[table] = fields
122
140
  end
141
+ (layout.portal_meta = @portal_meta) if layout and layout.portal_meta_no_load.blank?
123
142
  end
124
143
 
125
144
  def convert_date_time_format(fm_format)
@@ -107,13 +107,13 @@ module Rfm
107
107
  # * *name* is the name of this database
108
108
  # * *state* is a hash of all server options used to initialize this server
109
109
  class Server
110
- #
110
+
111
111
  # To create a Server object, you typically need at least a host name:
112
112
  #
113
113
  # myServer = Rfm::Server.new({:host => 'my.host.com'})
114
114
  #
115
- # Several other options are supported:
116
- #
115
+ # ===Several other options are supported
116
+ #
117
117
  # * *host* the hostname of the Web Publishing Engine (WPE) server (defaults to 'localhost')
118
118
  #
119
119
  # * *port* the port number the WPE is listening no (defaults to 80 unless *ssl* +true+ which sets it to 443)
@@ -138,13 +138,13 @@ module Rfm
138
138
  # ignores FileMaker's 401 error (no records found) and returns an empty record set instead; if you
139
139
  # prefer a raised error when a find produces no errors, set this option to +true+
140
140
  #
141
- # ===SSL Options (SSL AND CERTIFICATE VERIFICATION ARE ON BY DEFAULT):
142
- #
141
+ # ===SSL Options (SSL AND CERTIFICATE VERIFICATION ARE ON BY DEFAULT)
142
+ #
143
143
  # * *ssl* +false+ if you want to turn SSL (HTTPS) off when connecting to connect to FileMaker (default is +true+)
144
144
  #
145
- # If you are using SSL and want to verify the certificate use the following options:
145
+ # If you are using SSL and want to verify the certificate, use the following options:
146
146
  #
147
- # * *root_cert* +false+ if you do not want to verify your SSL session (default is +true+).
147
+ # * *root_cert* +true+ is the default. If you do not want to verify your SSL session, set this to +false+.
148
148
  # You will want to turn this off if you are using a self signed certificate and do not have a certificate authority cert file.
149
149
  # If you choose this option you will need to provide a cert *root_cert_name* and *root_cert_path* (if not in root directory).
150
150
  #
@@ -154,8 +154,8 @@ module Rfm
154
154
  #
155
155
  # * *root_cert_path* path to cert file. (defaults to '/' if no path given)
156
156
  #
157
- # ===Configuration Examples:
158
- #
157
+ # ===Configuration Examples
158
+ #
159
159
  # Example to turn off SSL:
160
160
  #
161
161
  # myServer = Rfm::Server.new({
@@ -192,8 +192,9 @@ module Rfm
192
192
  # :root_cert_name => 'example.pem'
193
193
  # :root_cert_path => '/usr/cert_file/'
194
194
  # })
195
-
196
195
  def initialize(options)
196
+ raise Rfm::Error::RfmError.new(0, "New instance of Rfm::Server has no host name.") if options[:host].to_s == ''
197
+
197
198
  @state = {
198
199
  :host => 'localhost',
199
200
  :port => 80,
@@ -205,17 +206,18 @@ module Rfm
205
206
  :password => '',
206
207
  :log_actions => false,
207
208
  :log_responses => false,
209
+ :log_parser => false,
208
210
  :warn_on_redirect => true,
209
211
  :raise_on_401 => false,
210
212
  :timeout => 60
211
213
  }.merge(options)
212
214
 
213
215
  @state.freeze
214
-
216
+
215
217
  @host_name = @state[:host]
216
218
  @scheme = @state[:ssl] ? "https" : "http"
217
219
  @port = @state[:ssl] && options[:port].nil? ? 443 : @state[:port]
218
-
220
+
219
221
  @db = Rfm::Factory::DbFactory.new(self)
220
222
  end
221
223
 
@@ -231,11 +233,13 @@ module Rfm
231
233
  # get no error at this point if the database you access doesn't exist. Instead, you'll
232
234
  # receive an error when you actually try to perform some action on a layout from this
233
235
  # database.
234
- def [](dbname)
235
- self.db[dbname]
236
- end
236
+ # def [](dbname, acnt=nil, pass=nil)
237
+ # self.db[dbname, acnt, pass]
238
+ # end
239
+ def_delegator :db, :[]
237
240
 
238
241
  attr_reader :db, :host_name, :port, :scheme, :state
242
+ alias_method :databases, :db
239
243
 
240
244
  # Performs a raw FileMaker action. You will generally not call this method directly, but it
241
245
  # is exposed in case you need to do something "under the hood."
@@ -272,12 +276,12 @@ module Rfm
272
276
  def load_layout(layout)
273
277
  post = {'-db' => layout.db.name, '-lay' => layout.name, '-view' => ''}
274
278
  resp = http_fetch(@host_name, @port, "/fmi/xml/FMPXMLLAYOUT.xml", layout.db.account_name, layout.db.password, post)
275
- remove_namespace(resp.body)
279
+ #remove_namespace(resp.body)
276
280
  end
277
281
 
278
282
  # Removes namespace from fmpxmllayout, so xpath will work
279
283
  def remove_namespace(xml)
280
- xml.gsub('xmlns="http://www.filemaker.com/fmpxmllayout"', '')
284
+ xml.gsub(/xmlns=\"[^\"]*\"/, '')
281
285
  end
282
286
 
283
287
  private
@@ -0,0 +1,64 @@
1
+ module Rfm
2
+
3
+ module ComplexQuery # @private :nodoc:
4
+ # Methods for Rfm::Layout to build complex queries
5
+ # Perform RFM find using complex boolean logic (multiple value options for a single field)
6
+ # Mimics creation of multiple find requests for "or" logic
7
+ # Use: rlayout_object.query({'fieldOne'=>['val1','val2','val3'], 'fieldTwo'=>'someValue', ...})
8
+ def query(hash_or_recid, options = {})
9
+ if hash_or_recid.kind_of? Hash
10
+ get_records('-findquery', assemble_query(hash_or_recid), options)
11
+ else
12
+ get_records('-find', {'-recid' => hash_or_recid.to_s}, options)
13
+ end
14
+ end
15
+
16
+ # Build ruby params to send to -query action via RFM
17
+ def assemble_query(query_hash)
18
+ key_values, query_map = build_key_values(query_hash)
19
+ key_values.merge("-query"=>query_translate(array_mix(query_map)))
20
+ end
21
+
22
+ # Build key-value definitions and query map '-q1...'
23
+ def build_key_values(qh)
24
+ key_values = {}
25
+ query_map = []
26
+ counter = 0
27
+ qh.each_with_index do |ha,i|
28
+ ha[1] = ha[1].to_a
29
+ query_tag = []
30
+ ha[1].each do |v|
31
+ key_values["-q#{counter}"] = ha[0]
32
+ key_values["-q#{counter}.value"] = v
33
+ query_tag << "q#{counter}"
34
+ counter += 1
35
+ end
36
+ query_map << query_tag
37
+ end
38
+ return key_values, query_map
39
+ end
40
+
41
+ # Build query request logic for FMP requests '-query...'
42
+ def array_mix(ary, line=[], rslt=[])
43
+ ary[0].to_a.each_with_index do |v,i|
44
+ array_mix(ary[1,ary.size], (line + [v]), rslt)
45
+ rslt << (line + [v]) if ary.size == 1
46
+ end
47
+ return rslt
48
+ end
49
+
50
+ # Translate query request logic to string
51
+ def query_translate(mixed_ary)
52
+ rslt = ""
53
+ sub = mixed_ary.collect {|a| "(#{a.join(',')})"}
54
+ sub.join(";")
55
+ end
56
+
57
+ end # ComplexQuery
58
+
59
+ # class Layout
60
+ # require 'rfm/layout'
61
+ # include ComplexQuery
62
+ # end
63
+
64
+ end # Rfm