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.
- data/CHANGELOG.md +107 -0
- data/README.md +378 -133
- data/lib/rfm.rb +51 -19
- data/lib/rfm/VERSION +1 -1
- data/lib/rfm/base.rb +416 -0
- data/lib/rfm/database.rb +14 -9
- data/lib/rfm/layout.rb +148 -96
- data/lib/rfm/metadata/field.rb +5 -5
- data/lib/rfm/metadata/field_control.rb +52 -51
- data/lib/rfm/metadata/script.rb +7 -5
- data/lib/rfm/record.rb +71 -56
- data/lib/rfm/resultset.rb +45 -26
- data/lib/rfm/server.rb +21 -17
- data/lib/rfm/utilities/complex_query.rb +64 -0
- data/lib/rfm/utilities/config.rb +115 -0
- data/lib/rfm/utilities/core_ext.rb +90 -0
- data/lib/rfm/utilities/factory.rb +100 -17
- data/lib/rfm/utilities/xml_parser.rb +94 -0
- data/lib/rfm/version.rb +1 -1
- data/lib/rfm/xml_mini/hpricot.rb +133 -0
- metadata +87 -30
data/lib/rfm.rb
CHANGED
@@ -3,34 +3,66 @@ module Rfm
|
|
3
3
|
$LOAD_PATH.unshift(PATH) unless $LOAD_PATH.include?(PATH)
|
4
4
|
end
|
5
5
|
|
6
|
+
require 'thread' # some versions of ActiveSupport will raise error about Mutex unless 'thread' is loaded.
|
7
|
+
require 'active_support'
|
8
|
+
# ActiveSupport appears to load these automatcially
|
9
|
+
#require 'active_support/core_ext/object/blank'
|
10
|
+
#require 'active_support/ordered_hash'
|
11
|
+
require 'active_support/version'
|
12
|
+
require 'rfm/utilities/core_ext'
|
6
13
|
require 'rfm/utilities/case_insensitive_hash'
|
7
|
-
require 'rfm/utilities/
|
8
|
-
require 'rfm/
|
9
|
-
|
14
|
+
#require 'rfm/utilities/config'
|
15
|
+
#require 'rfm/utilities/factory'
|
16
|
+
#require 'rfm/version.rb'
|
10
17
|
|
11
18
|
module Rfm
|
12
|
-
|
13
|
-
if $0.to_s.match(/irb|rails|bundle/) # was ENV['_']
|
14
|
-
puts "Using gem ginjo-rfm version: #{::Rfm::VERSION}"
|
15
|
-
end
|
16
19
|
|
17
20
|
class CommunicationError < StandardError; end
|
18
21
|
class ParameterError < StandardError; end
|
19
22
|
class AuthenticationError < StandardError; end
|
20
23
|
|
21
|
-
autoload :Error,
|
22
|
-
autoload :Server,
|
23
|
-
autoload :Database,
|
24
|
-
autoload :Layout,
|
25
|
-
autoload :Resultset,
|
26
|
-
autoload :Record,
|
24
|
+
autoload :Error, 'rfm/error'
|
25
|
+
autoload :Server, 'rfm/server'
|
26
|
+
autoload :Database, 'rfm/database'
|
27
|
+
autoload :Layout, 'rfm/layout'
|
28
|
+
autoload :Resultset, 'rfm/resultset'
|
29
|
+
autoload :Record, 'rfm/record'
|
30
|
+
autoload :Base, 'rfm/base'
|
31
|
+
autoload :XmlParser, 'rfm/utilities/xml_parser'
|
32
|
+
autoload :ComplexQuery, 'rfm/utilities/complex_query'
|
33
|
+
autoload :Config, 'rfm/utilities/config'
|
34
|
+
autoload :Factory, 'rfm/utilities/factory'
|
35
|
+
autoload :VERSION, 'rfm/version'
|
27
36
|
|
28
37
|
module Metadata
|
29
|
-
autoload :Script,
|
30
|
-
autoload :Field,
|
31
|
-
autoload :FieldControl,
|
32
|
-
autoload :ValueListItem,
|
38
|
+
autoload :Script, 'rfm/metadata/script'
|
39
|
+
autoload :Field, 'rfm/metadata/field'
|
40
|
+
autoload :FieldControl, 'rfm/metadata/field_control'
|
41
|
+
autoload :ValueListItem, 'rfm/metadata/value_list_item'
|
33
42
|
end
|
34
|
-
|
35
|
-
end
|
36
43
|
|
44
|
+
def info
|
45
|
+
rslt = <<-EEOOFF
|
46
|
+
Name: ginjo-rfm
|
47
|
+
Version: #{VERSION}
|
48
|
+
ActiveSupport Version: #{ActiveSupport::VERSION::STRING}
|
49
|
+
ActiveModel Loaded? #{defined?(ActiveModel) ? 'true' : 'false'}
|
50
|
+
ActiveModel Loadable? #{begin; require 'active_model'; rescue LoadError; $!; end}
|
51
|
+
XML Parser: #{XmlParser.backend}
|
52
|
+
EEOOFF
|
53
|
+
rslt.gsub!(/^[ \t]*/, '')
|
54
|
+
rescue
|
55
|
+
"Could not retrieve info: #{$!}"
|
56
|
+
end
|
57
|
+
|
58
|
+
def info_short
|
59
|
+
"Using ginjo-rfm version #{::Rfm::VERSION} with #{XmlParser.backend}"
|
60
|
+
end
|
61
|
+
|
62
|
+
def_delegators 'Rfm::Factory', :servers, :server, :db, :database, :layout, :models, :modelize
|
63
|
+
def_delegators 'Rfm::XmlParser', :backend, :backend=
|
64
|
+
def_delegators 'Rfm::Config', :config, :get_config, :config_clear
|
65
|
+
|
66
|
+
extend self
|
67
|
+
|
68
|
+
end # Rfm
|
data/lib/rfm/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
|
1
|
+
2.0.pre31
|
data/lib/rfm/base.rb
ADDED
@@ -0,0 +1,416 @@
|
|
1
|
+
module Rfm
|
2
|
+
#
|
3
|
+
# Adds ability to create Rfm::Base model classes that behave similar to ActiveRecord::Base models.
|
4
|
+
# If you set your Rfm.config (or RFM_CONFIG) with your host, database, account, password, and
|
5
|
+
# any other server/database options, you can provide your models with nothing more than a layout.
|
6
|
+
#
|
7
|
+
# class Person < Rfm::Base
|
8
|
+
# config :layout => 'mylayout'
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
# And similar to ActiveRecord, you can define callbacks, validations, attributes, and methods on your model.
|
12
|
+
#
|
13
|
+
# class Account < Rfm::Base
|
14
|
+
# config :layout=>'account_xml'
|
15
|
+
# before_create :encrypt_password
|
16
|
+
# validates :email, :presence => true
|
17
|
+
# validates :username, :presence => true
|
18
|
+
# attr_accessor :password
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# Then in your project, you can use these models just like ActiveRecord models.
|
22
|
+
# The query syntax and options are still Rfm under the hood. Treat your model
|
23
|
+
# classes like Rfm::Layout objects, with a few enhancements.
|
24
|
+
#
|
25
|
+
# @account = Account.new :username => 'bill', :password => 'pass'
|
26
|
+
# @account.email = 'my@email.com'
|
27
|
+
# @account.save!
|
28
|
+
#
|
29
|
+
# @person = Person.find({:name => 'mike'}, :max_records => 50)[0]
|
30
|
+
# @person.update_attributes(:name => 'Michael', :title => "Senior Partner")
|
31
|
+
# @person.save
|
32
|
+
#
|
33
|
+
#
|
34
|
+
#
|
35
|
+
#
|
36
|
+
# :nodoc: TODO: make sure all methods return something (a record?) if successful, otherwise nil or error
|
37
|
+
# :nodoc: TODO: move rfm methods & patches from fetch_mail_with_ruby to this file
|
38
|
+
#
|
39
|
+
# TODO: Are these really needed here?
|
40
|
+
require 'rfm/database'
|
41
|
+
require 'rfm/layout'
|
42
|
+
require 'rfm/record'
|
43
|
+
require 'rfm/utilities/factory'
|
44
|
+
require 'delegate'
|
45
|
+
|
46
|
+
|
47
|
+
|
48
|
+
class Layout
|
49
|
+
|
50
|
+
class SubLayout < DelegateClass(Layout)
|
51
|
+
include Layout::LayoutModule
|
52
|
+
attr_accessor :model, :parent_layout
|
53
|
+
|
54
|
+
def initialize(master)
|
55
|
+
super(master)
|
56
|
+
self.parent_layout = master
|
57
|
+
end
|
58
|
+
end # SubLayout
|
59
|
+
|
60
|
+
attr_accessor :subs
|
61
|
+
|
62
|
+
alias_method :main_init, :initialize
|
63
|
+
def initialize(*args)
|
64
|
+
@subs ||= []
|
65
|
+
main_init(*args)
|
66
|
+
end
|
67
|
+
|
68
|
+
def sublayout
|
69
|
+
if self.is_a?(Rfm::Layout)
|
70
|
+
sub = SubLayout.new(self); subs << sub; sub
|
71
|
+
else
|
72
|
+
self
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Creates new class with layout name, subclassed from Rfm::Base, and links the new model to a SubLayout instance.
|
77
|
+
def modelize
|
78
|
+
model_name = name.to_s.gsub(/\W/, '_').classify.gsub(/_/,'')
|
79
|
+
(return model_name.constantize) rescue nil
|
80
|
+
sub = sublayout
|
81
|
+
sub.instance_eval do
|
82
|
+
model_class = eval("::" + model_name + "= Class.new(Rfm::Base)")
|
83
|
+
model_class.class_exec(self) do |layout_obj|
|
84
|
+
@layout = layout_obj
|
85
|
+
end
|
86
|
+
@model = model_class
|
87
|
+
end
|
88
|
+
sub.model.to_s.constantize
|
89
|
+
rescue StandardError, SyntaxError
|
90
|
+
nil
|
91
|
+
end
|
92
|
+
|
93
|
+
def models
|
94
|
+
subs.collect{|s| s.model}
|
95
|
+
end
|
96
|
+
|
97
|
+
end # Layout
|
98
|
+
|
99
|
+
|
100
|
+
|
101
|
+
|
102
|
+
|
103
|
+
class Record
|
104
|
+
class << self
|
105
|
+
def new(*args)
|
106
|
+
#puts "Creating new record from RECORD. Layout: #{args[3].class} #{args[3].object_id}"
|
107
|
+
args[3].model.new(*args)
|
108
|
+
rescue
|
109
|
+
#puts "RECORD failed to send 'new' to MODEL"
|
110
|
+
super
|
111
|
+
#allocate.send(:initialize, *args)
|
112
|
+
end
|
113
|
+
end # class << self
|
114
|
+
end # class Record
|
115
|
+
|
116
|
+
|
117
|
+
|
118
|
+
|
119
|
+
|
120
|
+
class Database
|
121
|
+
def_delegators :layouts, :modelize, :models
|
122
|
+
end
|
123
|
+
|
124
|
+
|
125
|
+
|
126
|
+
|
127
|
+
module Factory
|
128
|
+
@models ||= []
|
129
|
+
|
130
|
+
class << self
|
131
|
+
attr_accessor :models
|
132
|
+
|
133
|
+
# Shortcut to Factory.db().layouts.modelize()
|
134
|
+
# If first parameter is regex, it is used for modelize filter.
|
135
|
+
# Otherwise, parameters are passed to Factory.database
|
136
|
+
def modelize(*args)
|
137
|
+
regx = args[0].is_a?(Regexp) ? args.shift : /.*/
|
138
|
+
db(*args).layouts.modelize(regx)
|
139
|
+
end
|
140
|
+
end # class << self
|
141
|
+
|
142
|
+
class LayoutFactory
|
143
|
+
def modelize(filter = /.*/)
|
144
|
+
all.values.each{|lay| lay.modelize if lay.name.match(filter)}
|
145
|
+
models
|
146
|
+
end
|
147
|
+
|
148
|
+
def models
|
149
|
+
rslt = {}
|
150
|
+
each do |k,lay|
|
151
|
+
layout_models = lay.models
|
152
|
+
rslt[k] = layout_models if !layout_models.blank?
|
153
|
+
end
|
154
|
+
rslt
|
155
|
+
end
|
156
|
+
|
157
|
+
end # LayoutFactory
|
158
|
+
end # Factory
|
159
|
+
|
160
|
+
|
161
|
+
|
162
|
+
|
163
|
+
class Base < Rfm::Record #Hash
|
164
|
+
extend Config
|
165
|
+
config :parent => 'Rfm::Config'
|
166
|
+
|
167
|
+
begin
|
168
|
+
require 'active_model'
|
169
|
+
include ActiveModel::Validations
|
170
|
+
include ActiveModel::Serialization
|
171
|
+
extend ActiveModel::Callbacks
|
172
|
+
define_model_callbacks(:create, :update, :destroy)
|
173
|
+
rescue LoadError, StandardError
|
174
|
+
def run_callbacks(*args)
|
175
|
+
yield
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def initialize(record={}, resultset_obj=[], field_meta='', layout_obj=self.class.layout, portal=nil)
|
180
|
+
if resultset_obj == [] and !record.has_key? 'field'
|
181
|
+
@mods = Rfm::CaseInsensitiveHash.new
|
182
|
+
@layout = layout_obj
|
183
|
+
@resultset = Resultset.allocate
|
184
|
+
# loop thru each layout field, creating hash keys with nil values
|
185
|
+
layout_obj.field_names.each do |field|
|
186
|
+
field_name = field.to_s
|
187
|
+
self[field_name] = nil
|
188
|
+
end
|
189
|
+
self.update_attributes(record) unless record == {}
|
190
|
+
self.merge!(@mods) unless @mods == {}
|
191
|
+
@loaded = true
|
192
|
+
else
|
193
|
+
super
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
|
198
|
+
class << self
|
199
|
+
|
200
|
+
# Access layout functions from base model
|
201
|
+
def_delegators :layout, :db, :server, :field_controls, :field_names, :value_lists, :total_count,
|
202
|
+
:query, :all, :delete, :portal_meta, :portal_names, :database, :table
|
203
|
+
|
204
|
+
def inherited(model)
|
205
|
+
(Rfm::Factory.models << model).uniq unless Rfm::Factory.models.include? model
|
206
|
+
model.config :parent=>'Rfm::Base'
|
207
|
+
end
|
208
|
+
|
209
|
+
# Build a new record without saving
|
210
|
+
def new(*args)
|
211
|
+
# Without this method, infinite recursion will happen from Record.new
|
212
|
+
#puts "Creating new record from BASE"
|
213
|
+
rec = self.allocate
|
214
|
+
rec.send(:initialize, *args)
|
215
|
+
rec
|
216
|
+
end
|
217
|
+
|
218
|
+
def config(*args)
|
219
|
+
super(*args){|strings| @config.merge!(:layout=>strings[0]) if strings[0]}
|
220
|
+
end
|
221
|
+
|
222
|
+
# Access/create the layout object associated with this model
|
223
|
+
def layout
|
224
|
+
return @layout if @layout
|
225
|
+
cnf = get_config
|
226
|
+
return unless cnf[:layout]
|
227
|
+
@layout = Rfm::Factory.layout(get_config).sublayout
|
228
|
+
@layout.model = self
|
229
|
+
@layout
|
230
|
+
end
|
231
|
+
|
232
|
+
# Just like Layout#find, but searching by record_id will return a record, not a resultset.
|
233
|
+
def find(*args)
|
234
|
+
r = layout.find(*args)
|
235
|
+
if args[0].class != Hash and r.size == 1
|
236
|
+
r[0]
|
237
|
+
else
|
238
|
+
r
|
239
|
+
end
|
240
|
+
rescue Rfm::Error::RecordMissingError
|
241
|
+
nil
|
242
|
+
end
|
243
|
+
|
244
|
+
# Layout#any, returns single record, not resultset
|
245
|
+
def any(*args)
|
246
|
+
layout.any(*args)[0]
|
247
|
+
end
|
248
|
+
|
249
|
+
# New record, save, (with callbacks & validations if ActiveModel is loaded)
|
250
|
+
def create(*args)
|
251
|
+
new(*args).send :create
|
252
|
+
end
|
253
|
+
|
254
|
+
# Using this method will skip callbacks. Use instance method +#update+ instead
|
255
|
+
def edit(*args)
|
256
|
+
layout.edit(*args)[0]
|
257
|
+
end
|
258
|
+
|
259
|
+
end # class << self
|
260
|
+
|
261
|
+
|
262
|
+
|
263
|
+
|
264
|
+
|
265
|
+
|
266
|
+
# Is this a newly created record, not saved yet?
|
267
|
+
def new_record?
|
268
|
+
return true if self.record_id.blank?
|
269
|
+
end
|
270
|
+
|
271
|
+
# Reload record from database
|
272
|
+
# TODO: handle error when record has been deleted
|
273
|
+
def reload(force=false)
|
274
|
+
if (@mods.empty? or force) and record_id
|
275
|
+
self.replace self.class.find(self.record_id)
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
# Mass update of record attributes, without saving.
|
280
|
+
# TODO: return error or nil if input hash contains no recognizable keys.
|
281
|
+
def update_attributes(new_attr)
|
282
|
+
# creates new special hash
|
283
|
+
input_hash = Rfm::CaseInsensitiveHash.new
|
284
|
+
# populate new hash with input, coercing keys to strings
|
285
|
+
new_attr.each{|k,v| input_hash.merge! k.to_s=>v}
|
286
|
+
# loop thru each layout field, adding data to @mods
|
287
|
+
self.class.field_controls.keys.each do |field|
|
288
|
+
field_name = field.to_s
|
289
|
+
if input_hash.has_key?(field_name)
|
290
|
+
@mods.merge! field_name=>(input_hash[field_name] || '')
|
291
|
+
end
|
292
|
+
end
|
293
|
+
# loop thru each input key-value,
|
294
|
+
# creating new attribute if key doesn't exist in model.
|
295
|
+
input_hash.each do |k,v|
|
296
|
+
if !self.class.field_controls.keys.include?(k) and self.respond_to?(k)
|
297
|
+
self.instance_variable_set("@#{k}", v)
|
298
|
+
end
|
299
|
+
end
|
300
|
+
self.merge!(@mods) unless @mods == {}
|
301
|
+
end
|
302
|
+
|
303
|
+
# Mass update of record attributes, with saving.
|
304
|
+
def update_attributes!(new_attr)
|
305
|
+
self.update_attributes(new_attr)
|
306
|
+
self.save!
|
307
|
+
end
|
308
|
+
|
309
|
+
# Save record modifications to database (with callbacks & validations). If record cannot be saved will raise error.
|
310
|
+
def save!
|
311
|
+
#return unless @mods.size > 0
|
312
|
+
raise "Record Invalid" unless valid? rescue nil
|
313
|
+
if @record_id
|
314
|
+
self.update
|
315
|
+
else
|
316
|
+
self.create
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
# Same as save!, but will not raise error.
|
321
|
+
def save
|
322
|
+
save!
|
323
|
+
rescue
|
324
|
+
(self.errors[:base] rescue []) << $!
|
325
|
+
return nil
|
326
|
+
end
|
327
|
+
|
328
|
+
# Just like Layout#save_if_not_modified, but with callbacks & validations.
|
329
|
+
def save_if_not_modified
|
330
|
+
update(@mod_id) if @mods.size > 0
|
331
|
+
end
|
332
|
+
|
333
|
+
# Delete record from database, with callbacks & validations.
|
334
|
+
def destroy
|
335
|
+
return unless record_id
|
336
|
+
run_callbacks :destroy do
|
337
|
+
self.class.delete(record_id)
|
338
|
+
@destroyed = true
|
339
|
+
@mods.clear
|
340
|
+
end
|
341
|
+
self.freeze
|
342
|
+
#self
|
343
|
+
end
|
344
|
+
|
345
|
+
def destroyed?
|
346
|
+
@destroyed
|
347
|
+
end
|
348
|
+
|
349
|
+
# For ActiveModel compatibility
|
350
|
+
def to_model
|
351
|
+
self
|
352
|
+
end
|
353
|
+
|
354
|
+
def persisted?
|
355
|
+
record_id ? true : false
|
356
|
+
end
|
357
|
+
|
358
|
+
def to_key
|
359
|
+
record_id ? [record_id] : nil
|
360
|
+
end
|
361
|
+
|
362
|
+
def to_param
|
363
|
+
record_id
|
364
|
+
end
|
365
|
+
|
366
|
+
|
367
|
+
protected # Base
|
368
|
+
|
369
|
+
def self.create_from_new(*args)
|
370
|
+
layout.create(*args)[0]
|
371
|
+
end
|
372
|
+
|
373
|
+
# shunt for callbacks when not using ActiveModel
|
374
|
+
def callback_deadend (*args)
|
375
|
+
yield #(*args)
|
376
|
+
end
|
377
|
+
|
378
|
+
def create
|
379
|
+
#return unless @mods.size > 0
|
380
|
+
run_callbacks :create do
|
381
|
+
return unless @mods.size > 0
|
382
|
+
merge_rfm_result self.class.create_from_new(@mods)
|
383
|
+
end
|
384
|
+
self
|
385
|
+
end
|
386
|
+
|
387
|
+
def update(mod_id=nil)
|
388
|
+
#return unless @mods.size > 0 and record_id
|
389
|
+
return false unless record_id
|
390
|
+
run_callbacks :update do
|
391
|
+
return unless @mods.size > 0
|
392
|
+
unless mod_id
|
393
|
+
# regular save
|
394
|
+
merge_rfm_result self.class.send :edit, record_id, @mods
|
395
|
+
else
|
396
|
+
# save_if_not_modified
|
397
|
+
merge_rfm_result self.class.send :edit, record_id, @mods, :modification_id=>mod_id
|
398
|
+
end
|
399
|
+
end
|
400
|
+
self
|
401
|
+
end
|
402
|
+
|
403
|
+
def merge_rfm_result(result_record)
|
404
|
+
return unless @mods.size > 0
|
405
|
+
@record_id ||= result_record.record_id
|
406
|
+
self.merge! result_record
|
407
|
+
@mods.clear
|
408
|
+
self || {}
|
409
|
+
end
|
410
|
+
|
411
|
+
end # Base
|
412
|
+
|
413
|
+
end # Rfm
|
414
|
+
|
415
|
+
|
416
|
+
|