ginjo-rfm 2.1.7 → 3.0.0
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.
- checksums.yaml +15 -0
- data/CHANGELOG.md +45 -16
- data/README.md +251 -274
- data/lib/rfm.rb +42 -20
- data/lib/rfm/VERSION +1 -1
- data/lib/rfm/base.rb +63 -196
- data/lib/rfm/database.rb +15 -16
- data/lib/rfm/layout.rb +244 -271
- data/lib/rfm/metadata/datum.rb +45 -0
- data/lib/rfm/metadata/field.rb +33 -13
- data/lib/rfm/metadata/field_control.rb +57 -25
- data/lib/rfm/metadata/layout_meta.rb +38 -0
- data/lib/rfm/metadata/resultset_meta.rb +66 -0
- data/lib/rfm/metadata/value_list_item.rb +7 -6
- data/lib/rfm/record.rb +54 -74
- data/lib/rfm/resultset.rb +63 -112
- data/lib/rfm/server.rb +6 -172
- data/lib/rfm/utilities/config.rb +100 -55
- data/lib/rfm/utilities/connection.rb +209 -0
- data/lib/rfm/utilities/core_ext.rb +14 -1
- data/lib/rfm/utilities/factory.rb +68 -65
- data/lib/rfm/utilities/sax_parser.rb +1039 -0
- metadata +154 -206
- data/lib/rfm/utilities/fmpxmlresult.rb +0 -167
- data/lib/rfm/utilities/fmresultset.rb +0 -153
- data/lib/rfm/utilities/xml_parser.rb +0 -124
- data/lib/rfm/xml_mini/hpricot.rb +0 -133
- data/lib/rfm/xml_mini/ox_sax.rb +0 -91
- data/lib/rfm/xml_mini/rexml_sax.rb +0 -81
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
module Rfm
|
3
|
+
module Metadata
|
4
|
+
|
5
|
+
class Datum #< DelegateClass(Field)
|
6
|
+
|
7
|
+
def get_mapped_name(name, resultset)
|
8
|
+
(resultset && resultset.layout && resultset.layout.field_mapping[name]) || name
|
9
|
+
end
|
10
|
+
|
11
|
+
def main_callback(cursor)
|
12
|
+
resultset = cursor.top.object
|
13
|
+
name = get_mapped_name(@attributes['name'].to_s, resultset)
|
14
|
+
field = resultset.field_meta[name]
|
15
|
+
data = @attributes['data']
|
16
|
+
cursor.parent.object[name.downcase] = field.coerce(data)
|
17
|
+
end
|
18
|
+
|
19
|
+
def portal_callback(cursor)
|
20
|
+
resultset = cursor.top.object
|
21
|
+
table, name = @attributes['name'].to_s.split('::')
|
22
|
+
#puts ['DATUM_portal_callback_01', table, name].join(', ')
|
23
|
+
name = get_mapped_name(name, resultset)
|
24
|
+
field = resultset.portal_meta[table.downcase][name.downcase]
|
25
|
+
data = @attributes['data']
|
26
|
+
#puts ['DATUM_portal_callback_02', resultset.class, table, name, field, data].join(', ')
|
27
|
+
#(y resultset.portal_meta) unless field
|
28
|
+
cursor.parent.object[name.downcase] = field.coerce(data)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Should return value only.
|
32
|
+
def handler_callback(cursor)
|
33
|
+
record = cursor.parent.object
|
34
|
+
resultset = cursor.top.object
|
35
|
+
|
36
|
+
name = get_mapped_name(@attributes['name'].to_s, resultset)
|
37
|
+
field = resultset.field_meta[name]
|
38
|
+
data = @attributes['data']
|
39
|
+
#puts ["\nDATUM", name, record.class, resultset.class, data]
|
40
|
+
record[name] = field.coerce(data)
|
41
|
+
end
|
42
|
+
|
43
|
+
end # Field
|
44
|
+
end # Metadata
|
45
|
+
end # Rfm
|
data/lib/rfm/metadata/field.rb
CHANGED
@@ -60,33 +60,53 @@ module Rfm
|
|
60
60
|
class Field
|
61
61
|
|
62
62
|
attr_reader :name, :result, :type, :max_repeats, :global
|
63
|
-
|
63
|
+
meta_attr_accessor :resultset
|
64
64
|
# Initializes a field object. You'll never need to do this. Instead, get your Field objects from
|
65
|
-
#
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
@max_repeats = field.max_repeats #['max-repeats']
|
71
|
-
@global = field.global #['global']
|
72
|
-
end
|
65
|
+
# Resultset::field_meta
|
66
|
+
def initialize(attributes)
|
67
|
+
_attach_as_instance_variables attributes
|
68
|
+
self
|
69
|
+
end
|
73
70
|
|
74
71
|
# Coerces the text value from an +fmresultset+ document into proper Ruby types based on the
|
75
72
|
# type of the field. You'll never need to do this: Rfm does it automatically for you when you
|
76
73
|
# access field data through the Record object.
|
77
|
-
def coerce(value
|
74
|
+
def coerce(value)
|
78
75
|
return nil if (value.nil? or value.empty?)
|
79
|
-
case
|
76
|
+
case result
|
80
77
|
when "text" then value
|
81
78
|
when "number" then BigDecimal.new(value.to_s)
|
82
79
|
when "date" then Date.strptime(value, resultset.date_format)
|
83
80
|
when "time" then DateTime.strptime("1/1/-4712 #{value}", "%m/%d/%Y #{resultset.time_format}")
|
84
81
|
when "timestamp" then DateTime.strptime(value, resultset.timestamp_format)
|
85
|
-
when "container" then
|
82
|
+
when "container" then
|
83
|
+
resultset_meta = resultset.instance_variable_get(:@meta)
|
84
|
+
if resultset_meta && resultset_meta['doctype'] && value.to_s[/\?/]
|
85
|
+
URI.parse(resultset_meta['doctype'].last.to_s).tap{|uri| uri.path, uri.query = value.split('?')}
|
86
|
+
else
|
87
|
+
value
|
88
|
+
end
|
86
89
|
else nil
|
87
90
|
end
|
88
|
-
|
91
|
+
# rescue
|
92
|
+
# puts("ERROR in Field#coerce:", name, value, result, $!)
|
93
|
+
# nil
|
94
|
+
end
|
95
|
+
|
96
|
+
def get_mapped_name
|
97
|
+
(resultset && resultset.layout && resultset.layout.field_mapping[name]) || name
|
89
98
|
end
|
99
|
+
|
100
|
+
def main_callback(cursor)
|
101
|
+
self.resultset = cursor.top.object
|
102
|
+
resultset.field_meta[get_mapped_name.to_s.downcase] = self
|
103
|
+
end
|
104
|
+
|
105
|
+
def portal_callback(cursor)
|
106
|
+
self.resultset = cursor.top.object
|
107
|
+
cursor.parent.object[get_mapped_name.split('::').last.to_s.downcase] = self
|
108
|
+
#puts ['FIELD_portal_callback', name, cursor.parent.object.object_id, cursor.parent.tag, cursor.parent.object[name.split('::').last.to_s.downcase]].join(', ')
|
109
|
+
end
|
90
110
|
|
91
111
|
end # Field
|
92
112
|
end # Metadata
|
@@ -22,33 +22,65 @@ module Rfm
|
|
22
22
|
# * *value_list* is an array of strings representing the value list items, or nil
|
23
23
|
# if this field has no attached value list
|
24
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
|
-
rfm_metaclass.instance_variable_set :@value_list, value_list
|
47
|
-
end
|
48
|
-
|
49
25
|
attr_reader :name, :style, :value_list_name
|
50
|
-
|
26
|
+
meta_attr_accessor :layout_meta
|
51
27
|
|
28
|
+
def initialize(attributes, meta)
|
29
|
+
self.layout_meta = meta
|
30
|
+
_attach_as_instance_variables attributes
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
34
|
+
# Handle manual attachment of STYLE element.
|
35
|
+
def handle_style_element(attributes)
|
36
|
+
_attach_as_instance_variables attributes, :key_translator=>method(:translate_value_list_key), :value_translator=>method(:translate_style_value)
|
37
|
+
end
|
38
|
+
|
39
|
+
def translate_style_value(raw)
|
40
|
+
#puts ["TRANSLATE_STYLE", raw].join(', ')
|
41
|
+
{
|
42
|
+
'EDITTEXT' => :edit_box,
|
43
|
+
'POPUPMENU' => :popup_menu,
|
44
|
+
'CHECKBOX' => :checkbox_set,
|
45
|
+
'RADIOBUTTONS' => :radio_button_set,
|
46
|
+
'POPUPLIST' => :popup_list,
|
47
|
+
'CALENDAR' => :calendar,
|
48
|
+
'SCROLLTEXT' => :scrollable,
|
49
|
+
}[raw] || raw
|
50
|
+
end
|
51
|
+
|
52
|
+
def translate_value_list_key(raw)
|
53
|
+
{'valuelist'=>'value_list_name'}[raw] || raw
|
54
|
+
end
|
55
|
+
|
56
|
+
def value_list
|
57
|
+
layout_meta.value_lists[value_list_name]
|
58
|
+
end
|
59
|
+
|
60
|
+
# def initialize(name, style, value_list_name, value_list)
|
61
|
+
# @name = name
|
62
|
+
# case style
|
63
|
+
# when "EDITTEXT"
|
64
|
+
# @style = :edit_box
|
65
|
+
# when "POPUPMENU"
|
66
|
+
# @style = :popup_menu
|
67
|
+
# when "CHECKBOX"
|
68
|
+
# @style = :checkbox_set
|
69
|
+
# when "RADIOBUTTONS"
|
70
|
+
# @style = :radio_button_set
|
71
|
+
# when "POPUPLIST"
|
72
|
+
# @style = :popup_list
|
73
|
+
# when "CALENDAR"
|
74
|
+
# @style = :calendar
|
75
|
+
# when "SCROLLTEXT"
|
76
|
+
# @style = :scrollable
|
77
|
+
# else
|
78
|
+
# nil
|
79
|
+
# end
|
80
|
+
# @value_list_name = value_list_name
|
81
|
+
# rfm_metaclass.instance_variable_set :@value_list, value_list
|
82
|
+
# end
|
83
|
+
|
52
84
|
end
|
53
85
|
end
|
54
86
|
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Rfm
|
2
|
+
module Metadata
|
3
|
+
class LayoutMeta < CaseInsensitiveHash
|
4
|
+
|
5
|
+
def initialize(layout)
|
6
|
+
@layout = layout
|
7
|
+
end
|
8
|
+
|
9
|
+
def field_controls
|
10
|
+
self['field_controls'] ||= CaseInsensitiveHash.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def field_names
|
14
|
+
field_controls.values.collect{|v| v.name}
|
15
|
+
end
|
16
|
+
|
17
|
+
def field_keys
|
18
|
+
field_controls.keys
|
19
|
+
end
|
20
|
+
|
21
|
+
def value_lists
|
22
|
+
self['value_lists'] ||= CaseInsensitiveHash.new
|
23
|
+
end
|
24
|
+
|
25
|
+
def handle_new_field_control(attributes)
|
26
|
+
name = attributes['name']
|
27
|
+
field_control = FieldControl.new(attributes, self)
|
28
|
+
field_controls[get_mapped_name(name)] = field_control
|
29
|
+
end
|
30
|
+
|
31
|
+
# Should this be in FieldControl object?
|
32
|
+
def get_mapped_name(name)
|
33
|
+
(@layout.field_mapping[name]) || name
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Rfm
|
2
|
+
module Metadata
|
3
|
+
class ResultsetMeta < CaseInsensitiveHash
|
4
|
+
|
5
|
+
def field_meta
|
6
|
+
self['field_meta'] ||= CaseInsensitiveHash.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def portal_meta
|
10
|
+
self['portal_meta'] ||= CaseInsensitiveHash.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def date_format
|
14
|
+
self['date_format']
|
15
|
+
end
|
16
|
+
|
17
|
+
def time_format
|
18
|
+
self['time_format']
|
19
|
+
end
|
20
|
+
|
21
|
+
def timestamp_format
|
22
|
+
self['timestamp_format']
|
23
|
+
end
|
24
|
+
|
25
|
+
def total_count
|
26
|
+
self['total_count'].to_i
|
27
|
+
end
|
28
|
+
|
29
|
+
def foundset_count
|
30
|
+
self['count'].to_i
|
31
|
+
end
|
32
|
+
|
33
|
+
def fetch_size
|
34
|
+
self['fetch_size'].to_i
|
35
|
+
end
|
36
|
+
|
37
|
+
def table
|
38
|
+
self['table']
|
39
|
+
end
|
40
|
+
|
41
|
+
def error
|
42
|
+
self['error']
|
43
|
+
end
|
44
|
+
|
45
|
+
def field_names
|
46
|
+
field_meta ? field_meta.values.collect{|v| v.name} : []
|
47
|
+
end
|
48
|
+
|
49
|
+
def field_keys
|
50
|
+
field_meta ? field_meta.keys : []
|
51
|
+
end
|
52
|
+
|
53
|
+
def portal_names
|
54
|
+
portal_meta ? portal_meta.keys : []
|
55
|
+
end
|
56
|
+
|
57
|
+
def handle_new_field(attributes)
|
58
|
+
f = Field.new(attributes)
|
59
|
+
# TODO: Re-enable these when you stop using the before_close callback.
|
60
|
+
# name = attributes['name']
|
61
|
+
# self[name] = f
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -15,14 +15,15 @@ module Rfm
|
|
15
15
|
#
|
16
16
|
# * *#value_list_name* is the name of the parent value list, if any
|
17
17
|
class ValueListItem < String
|
18
|
+
# TODO: re-instate saving of value_list_name.
|
18
19
|
attr_reader :value, :display, :value_list_name
|
19
20
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
21
|
+
# def initialize(value, display, value_list_name)
|
22
|
+
# @value_list_name = value_list_name
|
23
|
+
# @value = value.to_s
|
24
|
+
# @display = display.to_s
|
25
|
+
# self.replace @value
|
26
|
+
# end
|
26
27
|
|
27
28
|
end # ValueListItem
|
28
29
|
|
data/lib/rfm/record.rb
CHANGED
@@ -108,68 +108,48 @@ module Rfm
|
|
108
108
|
# copy of the same record
|
109
109
|
class Record < Rfm::CaseInsensitiveHash
|
110
110
|
|
111
|
-
attr_accessor :layout
|
111
|
+
attr_accessor :layout #, :resultset
|
112
112
|
attr_reader :record_id, :mod_id, :portals
|
113
|
-
def_delegators :
|
114
|
-
def_delegators :layout, :db, :database, :server
|
115
|
-
|
116
|
-
def initialize(record, resultset_obj, field_meta, layout_obj, portal=nil)
|
113
|
+
def_delegators :layout, :db, :database, :server, :field_meta, :portal_meta, :field_names, :portal_names
|
117
114
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
115
|
+
# This is called during the parsing process, but only to allow creation of the correct type of model instance.
|
116
|
+
# This is also called by the end-user when constructing a new empty record, but it is called from the model subclass.
|
117
|
+
def self.new(*args) # resultset
|
118
|
+
record = case
|
119
|
+
|
120
|
+
# Get model from layout, then allocate record.
|
121
|
+
when args[0].is_a?(Resultset) && args[0].layout
|
122
|
+
args[0].layout.modelize.allocate
|
123
|
+
|
124
|
+
# Allocate instance of Rfm::Record.
|
125
|
+
else
|
126
|
+
self.allocate
|
127
|
+
end
|
128
|
+
record.send(:initialize, *args)
|
129
|
+
record
|
130
|
+
# rescue
|
131
|
+
# puts "Record.new bombed and is defaulting to super.new. Error: #{$!}"
|
132
|
+
# super
|
133
|
+
end
|
124
134
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
begin
|
136
|
-
datum.push(field_meta[field_name].coerce(x, resultset_obj)) #(x['__content__'], resultset_obj))
|
137
|
-
rescue StandardError => error
|
138
|
-
self.errors.add(field_name, error) if self.respond_to? :errors
|
139
|
-
raise error unless @layout.ignore_bad_data
|
140
|
-
end
|
141
|
-
end if data
|
142
|
-
|
143
|
-
if datum.length == 1
|
144
|
-
rfm_super[field_name] = datum[0]
|
145
|
-
elsif datum.length == 0
|
146
|
-
rfm_super[field_name] = nil
|
147
|
-
else
|
148
|
-
rfm_super[field_name] = datum
|
135
|
+
def initialize(*args) # resultset, attributes
|
136
|
+
@mods ||= {}
|
137
|
+
@portals ||= Rfm::CaseInsensitiveHash.new
|
138
|
+
options = args.rfm_extract_options!
|
139
|
+
if args[0].is_a?(Resultset)
|
140
|
+
@layout = args[0].layout
|
141
|
+
elsif self.is_a?(Base)
|
142
|
+
@layout = self.class.layout
|
143
|
+
@layout.field_keys.each do |field|
|
144
|
+
self[field] = nil
|
149
145
|
end
|
146
|
+
self.update_attributes(options) unless options == {}
|
147
|
+
self.merge!(@mods) unless @mods == {}
|
148
|
+
@loaded = true
|
150
149
|
end
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
next if relatedset.blank?
|
155
|
-
tablename, records = relatedset.table, []
|
156
|
-
|
157
|
-
relatedset.records.each do |record|
|
158
|
-
next unless record
|
159
|
-
records << self.class.new(record, resultset_obj, resultset_obj.portal_meta[tablename], layout_obj, tablename)
|
160
|
-
end
|
161
|
-
|
162
|
-
@portals[tablename] = records
|
163
|
-
end
|
164
|
-
end
|
165
|
-
|
166
|
-
@loaded = true
|
167
|
-
end
|
168
|
-
|
169
|
-
def self.build_records(records, resultset_obj, field_meta, layout_obj, portal=nil)
|
170
|
-
records.each do |record|
|
171
|
-
resultset_obj << self.new(record, resultset_obj, field_meta, layout_obj, portal)
|
172
|
-
end
|
150
|
+
_attach_as_instance_variables args[1]
|
151
|
+
#@loaded = true
|
152
|
+
self
|
173
153
|
end
|
174
154
|
|
175
155
|
# Saves local changes to the Record object back to Filemaker. For example:
|
@@ -186,7 +166,8 @@ module Rfm
|
|
186
166
|
# to optimize on your end. Just save, and if you've changed the record it will be saved. If not, no
|
187
167
|
# server hit is incurred.
|
188
168
|
def save
|
189
|
-
self.merge!(layout.edit(self.record_id, @mods)[0]) if @mods.size > 0
|
169
|
+
# self.merge!(layout.edit(self.record_id, @mods)[0]) if @mods.size > 0
|
170
|
+
self.replace_with_fresh_data(layout.edit(self.record_id, @mods)[0]) if @mods.size > 0
|
190
171
|
@mods.clear
|
191
172
|
end
|
192
173
|
|
@@ -194,7 +175,8 @@ module Rfm
|
|
194
175
|
# modified after the record was fetched but before it was saved. In other words, prevents you from
|
195
176
|
# accidentally overwriting changes someone else made to the record.
|
196
177
|
def save_if_not_modified
|
197
|
-
self.merge!(layout.edit(@record_id, @mods, {:modification_id => @mod_id})[0]) if @mods.size > 0
|
178
|
+
# self.merge!(layout.edit(@record_id, @mods, {:modification_id => @mod_id})[0]) if @mods.size > 0
|
179
|
+
self.replace_with_fresh_data(layout.edit(@record_id, @mods, {:modification_id => @mod_id})[0]) if @mods.size > 0
|
198
180
|
@mods.clear
|
199
181
|
end
|
200
182
|
|
@@ -212,6 +194,8 @@ module Rfm
|
|
212
194
|
# When you do, the change is noted, but *the data is not updated in FileMaker*. You must call
|
213
195
|
# Record::save or Record::save_if_not_modified to actually save the data.
|
214
196
|
def [](key)
|
197
|
+
# Added by wbr, 2013-03-31
|
198
|
+
return super unless @loaded
|
215
199
|
return fetch(key.to_s.downcase)
|
216
200
|
rescue IndexError
|
217
201
|
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))
|
@@ -241,24 +225,20 @@ module Rfm
|
|
241
225
|
end
|
242
226
|
super(key, val)
|
243
227
|
end
|
244
|
-
#
|
245
|
-
# alias_method :old_setter, '[]='
|
246
|
-
# def []=(key,val)
|
247
|
-
# old_setter(key,val)
|
248
|
-
# return val unless [Date, Time, DateTime].member? val.class
|
249
|
-
# field_type = layout.field_meta[key.to_sym].result
|
250
|
-
# @mods[key] = case field_type
|
251
|
-
# when 'time'; val.strftime(layout.time_format)
|
252
|
-
# when 'date'; val.strftime(layout.date_format)
|
253
|
-
# when 'timestamp'; val.strftime(layout.timestamp_format)
|
254
|
-
# else val
|
255
|
-
# end
|
256
|
-
# end
|
257
228
|
|
258
229
|
def field_names
|
259
|
-
|
230
|
+
layout.field_names
|
260
231
|
end
|
261
|
-
|
232
|
+
|
233
|
+
def replace_with_fresh_data(record)
|
234
|
+
self.replace record
|
235
|
+
[:@mod_id, :@record_id, :@portals, :@mods].each do |var|
|
236
|
+
self.instance_variable_set var, record.instance_variable_get(var) || {}
|
237
|
+
end
|
238
|
+
self
|
239
|
+
end
|
240
|
+
|
241
|
+
|
262
242
|
|
263
243
|
private
|
264
244
|
|