ooor 1.9.2 → 2.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.
Files changed (45) hide show
  1. data/README.md +23 -71
  2. data/Rakefile +5 -0
  3. data/bin/ooor +1 -0
  4. data/lib/ooor.rb +87 -129
  5. data/lib/ooor/associations.rb +64 -0
  6. data/lib/ooor/base.rb +218 -0
  7. data/lib/{app/models → ooor}/base64.rb +0 -0
  8. data/lib/ooor/connection.rb +37 -0
  9. data/lib/ooor/errors.rb +120 -0
  10. data/lib/ooor/field_methods.rb +153 -0
  11. data/lib/{app → ooor}/helpers/core_helpers.rb +2 -2
  12. data/lib/ooor/locale.rb +13 -0
  13. data/lib/ooor/mini_active_resource.rb +94 -0
  14. data/lib/ooor/model_registry.rb +19 -0
  15. data/lib/ooor/naming.rb +73 -0
  16. data/lib/ooor/rack.rb +114 -0
  17. data/lib/ooor/railtie.rb +41 -0
  18. data/lib/ooor/reflection.rb +537 -0
  19. data/lib/ooor/reflection_ooor.rb +92 -0
  20. data/lib/{app/models → ooor}/relation.rb +61 -22
  21. data/lib/ooor/relation/finder_methods.rb +113 -0
  22. data/lib/ooor/report.rb +53 -0
  23. data/lib/{app/models → ooor}/serialization.rb +18 -6
  24. data/lib/ooor/services.rb +133 -0
  25. data/lib/ooor/session.rb +120 -0
  26. data/lib/ooor/session_handler.rb +63 -0
  27. data/lib/ooor/transport.rb +34 -0
  28. data/lib/ooor/transport/json_client.rb +53 -0
  29. data/lib/ooor/transport/xml_rpc_client.rb +15 -0
  30. data/lib/ooor/type_casting.rb +193 -0
  31. data/lib/ooor/version.rb +8 -0
  32. data/spec/helpers/test_helper.rb +11 -0
  33. data/spec/install_nightly.sh +17 -0
  34. data/spec/ooor_spec.rb +197 -79
  35. data/spec/requirements.txt +19 -0
  36. metadata +58 -20
  37. data/lib/app/models/client_xmlrpc.rb +0 -34
  38. data/lib/app/models/open_object_resource.rb +0 -486
  39. data/lib/app/models/services.rb +0 -47
  40. data/lib/app/models/type_casting.rb +0 -134
  41. data/lib/app/models/uml.rb +0 -210
  42. data/lib/app/ui/action_window.rb +0 -96
  43. data/lib/app/ui/client_base.rb +0 -36
  44. data/lib/app/ui/form_model.rb +0 -88
  45. data/ooor.yml +0 -27
data/lib/ooor/base.rb ADDED
@@ -0,0 +1,218 @@
1
+ # OOOR: OpenObject On Ruby
2
+ # Copyright (C) 2009-2013 Akretion LTDA (<http://www.akretion.com>).
3
+ # Author: Raphaël Valyi
4
+ # Licensed under the MIT license, see MIT-LICENSE file
5
+
6
+ require 'active_support/core_ext/hash/indifferent_access'
7
+ require 'active_support/core_ext/module/delegation.rb'
8
+ require 'ooor/reflection'
9
+ require 'ooor/reflection_ooor'
10
+
11
+ module Ooor
12
+ class ModelTemplate #meta data shared across sessions
13
+ TEMPLATE_PROPERTIES = [:openerp_id, :info, :access_ids, :description,
14
+ :openerp_model, :field_ids, :state, :fields,
15
+ :many2one_associations, :one2many_associations, :many2many_associations, :polymorphic_m2o_associations, :associations_keys,
16
+ :associations, :columns, :columns_hash]
17
+ attr_accessor *TEMPLATE_PROPERTIES
18
+ end
19
+
20
+
21
+ class Base < Ooor::MiniActiveResource
22
+ #PREDEFINED_INHERITS = {'product.product' => 'product_tmpl_id'}
23
+ include Naming, TypeCasting, Serialization, ReflectionOoor, Reflection, Associations, Report, FinderMethods, FieldMethods
24
+
25
+
26
+ # ********************** class methods ************************************
27
+ class << self
28
+
29
+ attr_accessor :name, :connection, :t, :scope_prefix #template
30
+ delegate *ModelTemplate::TEMPLATE_PROPERTIES, to: :t
31
+
32
+ # ******************** remote communication *****************************
33
+
34
+ def create(attributes = {}, context={}, default_get_list=false, reload=true)
35
+ self.new(attributes, default_get_list, context).tap { |resource| resource.save(context, reload) }
36
+ end
37
+
38
+ #OpenERP search method
39
+ def search(domain=[], offset=0, limit=false, order=false, context={}, count=false)
40
+ rpc_execute(:search, to_openerp_domain(domain), offset, limit, order, context, count)
41
+ end
42
+
43
+ def name_search(name='', domain=[], operator='ilike', context={}, limit=100)
44
+ rpc_execute(:name_search, name, to_openerp_domain(domain), operator, context, limit)
45
+ end
46
+
47
+ def rpc_execute(method, *args)
48
+ object_service(:execute, openerp_model, method, *args)
49
+ end
50
+
51
+ def rpc_exec_workflow(action, *args)
52
+ object_service(:exec_workflow, openerp_model, action, *args)
53
+ end
54
+
55
+ def object_service(service, obj, method, *args)
56
+ reload_fields_definition(false, connection.connection_session)
57
+ cast_answer_to_ruby!(connection.object.object_service(service, obj, method, *cast_request_to_openerp(args)))
58
+ end
59
+
60
+ def method_missing(method_symbol, *args)
61
+ raise RuntimeError.new("Invalid RPC method: #{method_symbol}") if [:type!, :allowed!].index(method_symbol)
62
+ self.rpc_execute(method_symbol.to_s, *args)
63
+ end
64
+
65
+ # ******************** AREL Minimal implementation ***********************
66
+
67
+ def relation(context={}); @relation ||= Relation.new(self, context); end #TODO template
68
+ def scoped(context={}); relation(context); end
69
+ def where(opts, *rest); relation.where(opts, *rest); end
70
+ def all(*args); relation.all(*args); end
71
+ def limit(value); relation.limit(value); end
72
+ def order(value); relation.order(value); end
73
+ def offset(value); relation.offset(value); end
74
+
75
+ def logger; Ooor.logger; end
76
+
77
+ end
78
+
79
+ self.name = "Base"
80
+
81
+ # ********************** instance methods **********************************
82
+
83
+ attr_accessor :associations, :loaded_associations, :ir_model_data_id, :object_session
84
+
85
+ def rpc_execute(method, *args)
86
+ args += [self.class.connection.connection_session.merge(object_session)] unless args[-1].is_a? Hash
87
+ self.class.object_service(:execute, self.class.openerp_model, method, *args)
88
+ end
89
+
90
+ def load(attributes, remove_root=false, persisted=false)#an attribute might actually be a association too, will be determined here
91
+ self.class.reload_fields_definition(false, object_session)
92
+ raise ArgumentError, "expected an attributes Hash, got #{attributes.inspect}" unless attributes.is_a?(Hash)
93
+ @prefix_options, attributes = split_options(attributes)
94
+ @associations ||= {}
95
+ @attributes ||= {}
96
+ @loaded_associations = {}
97
+ attributes.each do |key, value|
98
+ skey = key.to_s
99
+ if self.class.associations_keys.index(skey) || value.is_a?(Array) #FIXME may miss m2o with inherits!
100
+ @associations[skey] = value #the association because we want the method to load the association through method missing
101
+ else
102
+ @attributes[skey] = value || nil #don't bloat with false values
103
+ end
104
+ end
105
+ self
106
+ end
107
+
108
+ #takes care of reading OpenERP default field values.
109
+ def initialize(attributes = {}, default_get_list=false, context={}, persisted=false)
110
+ @attributes = {}
111
+ @prefix_options = {}
112
+ @ir_model_data_id = attributes.delete(:ir_model_data_id)
113
+ @object_session = {}
114
+ @object_session = HashWithIndifferentAccess.new(context)
115
+ @persisted = persisted
116
+ self.class.reload_fields_definition(false, @object_session)
117
+ if default_get_list == []
118
+ load(attributes)
119
+ else
120
+ load_with_defaults(attributes, default_get_list)
121
+ end
122
+ end
123
+
124
+ def save(context={}, reload=true)
125
+ new? ? create(context, reload) : update(context, reload)
126
+ rescue ValidationError => e
127
+ e.extract_validation_error!(errors)
128
+ return false
129
+ end
130
+
131
+ #compatible with the Rails way but also supports OpenERP context
132
+ def create(context={}, reload=true)
133
+ self.id = rpc_execute('create', to_openerp_hash, context)
134
+ if @ir_model_data_id
135
+ IrModelData.create(model: self.class.openerp_model,
136
+ 'module' => @ir_model_data_id[0],
137
+ 'name' => @ir_model_data_id[1],
138
+ 'res_id' => self.id)
139
+ end
140
+ reload_from_record!(self.class.find(self.id, context: context)) if reload
141
+ @persisted = true
142
+ end
143
+
144
+ def update_attributes(attributes, context={}, reload=true)
145
+ load(attributes, false) && save(context, reload)
146
+ end
147
+
148
+ #compatible with the Rails way but also supports OpenERP context
149
+ def update(context={}, reload=true) #TODO use http://apidock.com/rails/ActiveRecord/Dirty to minimize data to save back
150
+ rpc_execute('write', [self.id], to_openerp_hash, context)
151
+ reload_fields(context) if reload
152
+ @persisted = true
153
+ end
154
+
155
+ #compatible with the Rails way but also supports OpenERP context
156
+ def destroy(context={})
157
+ rpc_execute('unlink', [self.id], context)
158
+ end
159
+
160
+ #OpenERP copy method, load persisted copied Object
161
+ def copy(defaults={}, context={})
162
+ self.class.find(rpc_execute('copy', self.id, defaults, context), context: context)
163
+ end
164
+
165
+ #Generic OpenERP rpc method call
166
+ def call(method, *args) rpc_execute(method, *args) end
167
+
168
+ #Generic OpenERP on_change method
169
+ def on_change(on_change_method, field_name, field_value, *args)
170
+ # NOTE: OpenERP doesn't accept context systematically in on_change events unfortunately
171
+ ids = self.id ? [id] : []
172
+ result = self.class.object_service(:execute, self.class.openerp_model, on_change_method, ids, *args)
173
+ load_on_change_result(result, field_name, field_value)
174
+ end
175
+
176
+ #wrapper for OpenERP exec_workflow Business Process Management engine
177
+ def wkf_action(action, context={}, reload=true)
178
+ self.class.object_service(:exec_workflow, self.class.openerp_model, action, self.id, object_session)
179
+ reload_fields(context) if reload
180
+ end
181
+
182
+ #Add get_report_data to obtain [report["result"],report["format]] of a concrete openERP Object
183
+ def get_report_data(report_name, report_type="pdf", context={})
184
+ self.class.get_report_data(report_name, [self.id], report_type, context)
185
+ end
186
+
187
+ def type() method_missing(:type) end #skips deprecated Object#type method
188
+
189
+ private
190
+
191
+ def load_with_defaults(attributes, default_get_list)
192
+ defaults = rpc_execute("default_get", default_get_list || self.class.fields.keys + self.class.associations_keys, object_session.dup)
193
+ attributes = HashWithIndifferentAccess.new(defaults.merge(attributes.reject {|k, v| v.blank? }))
194
+ load(attributes)
195
+ end
196
+
197
+ def load_on_change_result(result, field_name, field_value)
198
+ if result["warning"]
199
+ self.class.logger.info result["warning"]["title"]
200
+ self.class.logger.info result["warning"]["message"]
201
+ end
202
+ attrs = @attributes.merge(field_name => field_value)
203
+ attrs.merge!(result["value"])
204
+ load(attrs)
205
+ end
206
+
207
+ # Ruby 1.9.compat, See also http://tenderlovemaking.com/2011/06/28/til-its-ok-to-return-nil-from-to_ary/
208
+ def to_ary; nil; end # :nodoc:
209
+
210
+ def reload_from_record!(record) load(record.attributes.merge(record.associations)) end
211
+
212
+ def reload_fields(context)
213
+ records = self.class.find(self.id, context: context, fields: @attributes.keys + @associations.keys)
214
+ reload_from_record!(records)
215
+ end
216
+
217
+ end
218
+ end
File without changes
@@ -0,0 +1,37 @@
1
+ # OOOR: OpenObject On Ruby
2
+ # Copyright (C) 2009-2012 Akretion LTDA (<http://www.akretion.com>).
3
+ # Author: Raphaël Valyi
4
+ # Licensed under the MIT license, see MIT-LICENSE file
5
+
6
+ require 'active_support/core_ext/hash/indifferent_access'
7
+
8
+ module Ooor
9
+ class Connection
10
+ attr_accessor :config, :connection_session
11
+
12
+ def initialize(config, env=false)
13
+ @config = _config(config)
14
+ Object.const_set(@config[:scope_prefix], Module.new) if @config[:scope_prefix]
15
+ end
16
+
17
+ def connection_session
18
+ @connection_session ||= {}.merge!(@config[:connection_session] || {})
19
+ end
20
+
21
+ def helper_paths
22
+ [File.dirname(__FILE__) + '/helpers/*', *@config[:helper_paths]]
23
+ end
24
+
25
+ def class_name_from_model_key(model_key)
26
+ model_key.split('.').collect {|name_part| name_part.capitalize}.join
27
+ end
28
+
29
+ private
30
+
31
+ def _config(config)
32
+ c = config.is_a?(String) ? Ooor.load_config(config, env) : config
33
+ HashWithIndifferentAccess.new(c)
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,120 @@
1
+ module Ooor
2
+ class OpenERPServerError < RuntimeError
3
+ attr_accessor :request, :faultCode, :faultString
4
+
5
+ def self.create_from_trace(error, method, *args)
6
+ begin
7
+ #extracts the eventual error log from OpenERP response as OpenERP doesn't enforce carefully*
8
+ #the XML/RPC spec, see https://bugs.launchpad.net/openerp/+bug/257581
9
+ openerp_error_hash = eval("#{error}".gsub("wrong fault-structure: ", ""))
10
+ rescue SyntaxError
11
+ end
12
+ if openerp_error_hash.is_a? Hash
13
+ build(openerp_error_hash['faultCode'], openerp_error_hash['faultString'], method, *args)
14
+ else
15
+ return UnknownOpenERPServerError.new("method: #{method} - args: #{args.inspect}")
16
+ end
17
+ end
18
+
19
+ def self.build(faultCode, faultString, method, *args)
20
+ if faultCode =~ /AttributeError: /
21
+ return UnknownAttributeOrAssociationError.new("method: #{method} - args: #{args.inspect}", faultCode, faultString)
22
+ elsif faultCode =~ /TypeError: /
23
+ return TypeError.new(method, faultCode, faultString, *args)
24
+ elsif faultCode =~ /ValueError: /
25
+ return ValueError.new(method, faultCode, faultString, *args)
26
+ elsif faultCode =~ /ValidateError/
27
+ return ValidationError.new(method, faultCode, faultString, *args)
28
+ elsif faultCode =~ /AccessDenied/
29
+ return UnAuthorizedError.new(method, faultCode, faultString, *args)
30
+ elsif faultCode =~ /AuthenticationError: Credentials not provided/
31
+ return InvalidSessionError.new(method, faultCode, faultString, *args)
32
+ elsif faultCode =~ /SessionExpiredException/
33
+ return SessionExpiredError.new(method, faultCode, faultString, *args)
34
+ else
35
+ return new(method, faultCode, faultString, *args)
36
+ end
37
+ end
38
+
39
+ def initialize(method=nil, faultCode=nil, faultString=nil, *args)
40
+ filtered_args = filter_password(args.dup())
41
+ @request = "method: #{method} - args: #{filtered_args.inspect}"
42
+ @faultCode = faultCode
43
+ @faultString = faultString
44
+ super()
45
+ end
46
+
47
+ def filter_password(args)
48
+ if args[0].is_a?(String) && (args[1].is_a?(Integer) || args[1].to_i != 0) && args[2].is_a?(String)
49
+ args[2] = "####"
50
+ end
51
+ args.map! do |arg|
52
+ if arg.is_a?(Hash)# && (arg.keys.index('password') || arg.keys.index(:password))
53
+ r = {}
54
+ arg.each do |k, v|
55
+ if k.to_s.index('password')
56
+ r[k] = "####"
57
+ else
58
+ r[k] = v
59
+ end
60
+ end
61
+ r
62
+ else
63
+ arg
64
+ end
65
+ end
66
+ end
67
+
68
+ def to_s()
69
+ s = super
70
+ line = "********************************************"
71
+ s = "\n\n#{line}\n*********** OOOR Request ***********\n#{@request}\n#{line}\n\n"
72
+ s << "\n#{line}\n*********** OpenERP Server ERROR ***********\n#{line}\n#{@faultCode}\n#{@faultString}\n#{line}\n."
73
+ s
74
+ end
75
+
76
+ end
77
+
78
+
79
+ class UnknownOpenERPServerError < OpenERPServerError; end
80
+ class UnAuthorizedError < OpenERPServerError; end
81
+ class TypeError < OpenERPServerError; end
82
+ class ValueError < OpenERPServerError; end
83
+ class InvalidSessionError < OpenERPServerError; end
84
+ class SessionExpiredError < OpenERPServerError; end
85
+
86
+ class ValidationError < OpenERPServerError
87
+ def extract_validation_error!(errors)
88
+ @faultCode.split("\n").each do |line|
89
+ extract_error_line!(errors, line) if line.index(': ')
90
+ end
91
+ end
92
+
93
+ def extract_error_line!(errors, line)
94
+ fields = line.split(": ")[0].split(' ').last.split(',')
95
+ msg = line.split(": ")[1]
96
+ fields.each { |field| errors.add(field.strip.to_sym, msg) }
97
+ end
98
+ end
99
+
100
+ class UnknownAttributeOrAssociationError < OpenERPServerError
101
+ attr_accessor :klass
102
+
103
+ def to_s()
104
+ s = super
105
+ s << available_fields(@klass) if @klass
106
+ s
107
+ end
108
+
109
+ def available_fields(clazz)
110
+ msg = "\n\n*** AVAILABLE FIELDS ON #{clazz.name} ARE: ***"
111
+ msg << "\n\n" << clazz.t.fields.sort {|a,b| a[1]['type']<=>b[1]['type']}.map {|i| "#{i[1]['type']} --- #{i[0]}"}.join("\n")
112
+ %w[many2one one2many many2many polymorphic_m2o].each do |kind|
113
+ msg << "\n\n"
114
+ msg << (clazz.send "#{kind}_associations").map {|k, v| "#{kind} --- #{v['relation']} --- #{k}"}.join("\n")
115
+ end
116
+ msg
117
+ end
118
+ end
119
+
120
+ end
@@ -0,0 +1,153 @@
1
+ require 'active_support/concern'
2
+
3
+ module Ooor
4
+ module FieldMethods
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+
9
+ def reload_fields_definition(force=false, context=connection.web_session)
10
+ if force || !fields
11
+ @t.fields = {}
12
+ @columns_hash = {}
13
+ fields_get = rpc_execute("fields_get", false, context)
14
+ fields_get.each { |k, field| reload_field_definition(k, field) }
15
+ @t.associations_keys = many2one_associations.keys + one2many_associations.keys + many2many_associations.keys + polymorphic_m2o_associations.keys
16
+ (fields.keys + associations_keys).each do |meth| #generates method handlers for auto-completion tools
17
+ define_field_method(meth)
18
+ end
19
+ one2many_associations.keys.each do |meth|
20
+ define_nested_attributes_method(meth)
21
+ end
22
+ logger.debug "#{fields.size} fields loaded in model #{self.name}"
23
+ Ooor.model_registry.set_template(connection.config, @t)
24
+ end
25
+ end
26
+
27
+ def all_fields
28
+ fields.merge(polymorphic_m2o_associations).merge(many2many_associations).merge(one2many_associations).merge(many2one_associations)
29
+ end
30
+
31
+ def fast_fields(options)
32
+ fields = all_fields
33
+ fields.keys.select do |k|
34
+ fields[k]["type"] != "binary" && (options[:include_functions] || !fields[k]["function"])
35
+ end
36
+ end
37
+
38
+ # this is used by fields_for in ActionView FormHelper
39
+ def define_nested_attributes_method(meth)
40
+ p "define_nested_attributes_method", meth
41
+ unless self.respond_to?(meth)
42
+ self.instance_eval do
43
+ define_method "#{meth}_attributes=" do |*args|
44
+ self.send :method_missing, *[meth, *args]
45
+ end
46
+ define_method "#{meth}_attributes" do |*args|
47
+ self.send :method_missing, *[meth, *args]
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def define_field_method(meth)
56
+ unless self.respond_to?(meth)
57
+ self.instance_eval do
58
+ define_method meth do |*args|
59
+ self.send :method_missing, *[meth, *args]
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ def reload_field_definition(k, field)
66
+ case field['type']
67
+ when 'many2one'
68
+ many2one_associations[k] = field
69
+ when 'one2many'
70
+ one2many_associations[k] = field
71
+ when 'many2many'
72
+ many2many_associations[k] = field
73
+ when 'reference'
74
+ polymorphic_m2o_associations[k] = field
75
+ else
76
+ fields[k] = field if field['name'] != 'id'
77
+ end
78
+ end
79
+ end
80
+
81
+ def method_missing(method_symbol, *arguments)
82
+ method_name = method_symbol.to_s
83
+ method_key = method_name.sub('=', '')
84
+ self.class.reload_fields_definition(false, object_session)
85
+ if attributes.has_key?(method_key)
86
+ if method_name.end_with?('=')
87
+ attributes[method_key] = arguments[0]
88
+ else
89
+ attributes[method_key]
90
+ end
91
+ elsif @loaded_associations.has_key?(method_name)
92
+ @loaded_associations[method_name]
93
+ elsif @associations.has_key?(method_name)
94
+ result = relationnal_result(method_name, *arguments)
95
+ @loaded_associations[method_name] = result and return result if result
96
+ elsif method_name.end_with?('=')
97
+ return method_missing_value_assign(method_key, arguments)
98
+ elsif self.class.fields.has_key?(method_name) || self.class.associations_keys.index(method_name) #unloaded field/association
99
+ return lazzy_load_field(method_name, *arguments)
100
+ # check if that is not a Rails style association with an _id[s][=] suffix:
101
+ elsif method_name.match(/_id$/) && self.class.associations_keys.index(rel=method_name.gsub(/_id$/, ""))
102
+ return many2one_id_method(rel, *arguments)
103
+ elsif method_name.match(/_ids$/) && self.class.associations_keys.index(rel=method_name.gsub(/_ids$/, ""))
104
+ return x_to_many_ids_method(rel, *arguments)
105
+ elsif id
106
+ rpc_execute(method_key, [id], *arguments) #we assume that's an action
107
+ else
108
+ super
109
+ end
110
+
111
+ rescue UnknownAttributeOrAssociationError => e
112
+ e.klass = self.class
113
+ raise e
114
+ end
115
+
116
+ private
117
+
118
+ def method_missing_value_assign(method_key, arguments)
119
+ if is_association_assignment(method_key)
120
+ @associations[method_key] = arguments[0]
121
+ @loaded_associations[method_key] = arguments[0]
122
+ elsif is_attribute_assignment(method_key)
123
+ @attributes[method_key] = arguments[0]
124
+ end
125
+ end
126
+
127
+ def is_association_assignment(method_key)
128
+ (self.class.associations_keys + self.class.many2one_associations.collect do |k, field|
129
+ klass = self.class.const_get(field['relation'])
130
+ klass.reload_fields_definition(false, object_session)
131
+ klass.t.associations_keys
132
+ end.flatten).index(method_key)
133
+ end
134
+
135
+ def is_attribute_assignment(method_key)
136
+ (self.class.fields.keys + self.class.many2one_associations.collect do |k, field|
137
+ klass = self.class.const_get(field['relation'])
138
+ klass.reload_fields_definition(false, object_session)
139
+ klass.t.fields.keys
140
+ end.flatten).index(method_key)
141
+ end
142
+
143
+ def lazzy_load_field(field_name, *arguments)
144
+ if attributes["id"]
145
+ load(rpc_execute('read', [id], [field_name], *arguments || object_session)[0] || {})
146
+ method_missing(field_name, *arguments)
147
+ else
148
+ nil
149
+ end
150
+ end
151
+
152
+ end
153
+ end