kangaroo 0.0.1.pre

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 (68) hide show
  1. data/.gitignore +1 -0
  2. data/.rspec +2 -0
  3. data/.yardopts +6 -0
  4. data/Gemfile +2 -0
  5. data/Gemfile.lock +66 -0
  6. data/LICENSE +22 -0
  7. data/README.md +65 -0
  8. data/Rakefile +36 -0
  9. data/config/kangaroo.yml.sample +12 -0
  10. data/docs/AdditionalServices.md +3 -0
  11. data/docs/Architecture.md +50 -0
  12. data/docs/Installation.md +73 -0
  13. data/docs/Usage.md +161 -0
  14. data/features/connection.feature +5 -0
  15. data/features/env.rb +8 -0
  16. data/features/step_definitions/basic_steps.rb +24 -0
  17. data/features/step_definitions/connection_steps.rb +13 -0
  18. data/features/support/test.yml +11 -0
  19. data/features/utility_services.feature +39 -0
  20. data/kangaroo.gemspec +29 -0
  21. data/lib/kangaroo.rb +6 -0
  22. data/lib/kangaroo/exception.rb +7 -0
  23. data/lib/kangaroo/model/attributes.rb +124 -0
  24. data/lib/kangaroo/model/base.rb +70 -0
  25. data/lib/kangaroo/model/condition_normalizer.rb +63 -0
  26. data/lib/kangaroo/model/default_attributes.rb +22 -0
  27. data/lib/kangaroo/model/field.rb +20 -0
  28. data/lib/kangaroo/model/finder.rb +92 -0
  29. data/lib/kangaroo/model/inspector.rb +55 -0
  30. data/lib/kangaroo/model/open_object_orm.rb +117 -0
  31. data/lib/kangaroo/model/persistence.rb +180 -0
  32. data/lib/kangaroo/model/relation.rb +212 -0
  33. data/lib/kangaroo/model/remote_execute.rb +29 -0
  34. data/lib/kangaroo/railtie.rb +13 -0
  35. data/lib/kangaroo/ruby_adapter/base.rb +28 -0
  36. data/lib/kangaroo/ruby_adapter/class_definition.rb +46 -0
  37. data/lib/kangaroo/ruby_adapter/fields.rb +18 -0
  38. data/lib/kangaroo/util/client.rb +59 -0
  39. data/lib/kangaroo/util/configuration.rb +82 -0
  40. data/lib/kangaroo/util/database.rb +92 -0
  41. data/lib/kangaroo/util/loader.rb +98 -0
  42. data/lib/kangaroo/util/loader/model.rb +21 -0
  43. data/lib/kangaroo/util/loader/namespace.rb +15 -0
  44. data/lib/kangaroo/util/proxy.rb +35 -0
  45. data/lib/kangaroo/util/proxy/common.rb +111 -0
  46. data/lib/kangaroo/util/proxy/db.rb +34 -0
  47. data/lib/kangaroo/util/proxy/object.rb +153 -0
  48. data/lib/kangaroo/util/proxy/report.rb +24 -0
  49. data/lib/kangaroo/util/proxy/superadmin.rb +71 -0
  50. data/lib/kangaroo/util/proxy/wizard.rb +25 -0
  51. data/lib/kangaroo/util/proxy/workflow.rb +14 -0
  52. data/lib/kangaroo/version.rb +3 -0
  53. data/spec/model/attributes_spec.rb +70 -0
  54. data/spec/model/base_spec.rb +19 -0
  55. data/spec/model/default_attributes_spec.rb +37 -0
  56. data/spec/model/finder_spec.rb +104 -0
  57. data/spec/model/inspector_spec.rb +56 -0
  58. data/spec/model/open_object_orm_spec.rb +134 -0
  59. data/spec/model/persistence_spec.rb +53 -0
  60. data/spec/model/relation_spec.rb +122 -0
  61. data/spec/ruby_adapter/class_definition_spec.rb +51 -0
  62. data/spec/server_helper.rb +167 -0
  63. data/spec/spec_helper.rb +14 -0
  64. data/spec/test_env/test.yml +11 -0
  65. data/spec/util/configuration_spec.rb +36 -0
  66. data/spec/util/loader_spec.rb +50 -0
  67. data/spec/util/proxy_spec.rb +61 -0
  68. metadata +260 -0
@@ -0,0 +1,92 @@
1
+ require 'active_support/core_ext/module'
2
+
3
+ module Kangaroo
4
+ module Model
5
+ module Finder
6
+ RELATION_DELEGATES = %w(where limit offset order select context reverse)
7
+ delegate *(RELATION_DELEGATES + [:to => :relation])
8
+
9
+ # Retrieve all records
10
+ #
11
+ # @return [Array] records
12
+ def all
13
+ relation.all
14
+ end
15
+
16
+ # ActiveRecord-ish find method
17
+ #
18
+ # @overload find(id)
19
+ # Find a record by id
20
+ # @param [Number] id
21
+ # @overload find(keyword)
22
+ # Find all, first or last record
23
+ # @param [String, Symbol] keyword :all, :first or :last
24
+ def find id_or_keyword
25
+ case id_or_keyword
26
+ when :all, 'all'
27
+ all
28
+ when :first, 'first'
29
+ first
30
+ when :last, 'last'
31
+ last
32
+ else
33
+ super
34
+ end
35
+ end
36
+
37
+ def exists? ids
38
+ where(:id => ids).exists?
39
+ end
40
+
41
+ # Retrieve first record
42
+ #
43
+ # @return record
44
+ def first
45
+ relation.first
46
+ end
47
+
48
+ # Retrieve last record
49
+ #
50
+ # @return record
51
+ def last
52
+ relation.last
53
+ end
54
+
55
+ # Count number of records
56
+ #
57
+ # @return [Number]
58
+ def count
59
+ count_by
60
+ end
61
+
62
+ # @private
63
+ # Search, read and instantiate records at once
64
+ #
65
+ # @param [Array, Hash, String] conditions
66
+ # @param [Hash] search_options
67
+ # @param [Hash] read_options
68
+ # @return [Array]
69
+ def search_and_read conditions, search_options = {}, read_options = {}
70
+ ids = search conditions, search_options.merge(:count => false)
71
+ return [] if ids.blank?
72
+
73
+ read ids, read_options
74
+ end
75
+
76
+ # @private
77
+ # Count objects matching the conditions
78
+ #
79
+ # @param [Array, Hash, String] conditions
80
+ # @param [Hash] search options
81
+ # @return [Number]
82
+ def count_by conditions = [], search_options = {}
83
+ search conditions, search_options.merge(:count => true)
84
+ end
85
+
86
+ # @private
87
+ def relation
88
+ Relation.new self
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,55 @@
1
+ module Kangaroo
2
+ module Model
3
+ module Inspector
4
+ # @private
5
+ def self.included klass
6
+ klass.extend ClassMethods
7
+ end
8
+
9
+ # @private
10
+ def inspect
11
+ inspect_wrap do |str|
12
+ str << [inspect_id, *inspect_attributes].join(', ')
13
+ end
14
+ end
15
+
16
+ private
17
+ def inspect_id
18
+ "id: #{id.inspect}"
19
+ end
20
+
21
+ def inspect_attributes
22
+ attributes.to_a.map do |key_val|
23
+ name, value = key_val
24
+ [name, value.inspect].join ": "
25
+ end
26
+ end
27
+
28
+ def inspect_wrap
29
+ "".tap do |str|
30
+ str << "<#{self.class.name} "
31
+ yield str
32
+ str << ">"
33
+ end
34
+ end
35
+
36
+ # @private
37
+ module ClassMethods
38
+ def inspect
39
+ inspect_wrap do |str|
40
+ str << ['id', *attribute_names].join(', ')
41
+ end
42
+ end
43
+
44
+ private
45
+ def inspect_wrap
46
+ "".tap do |str|
47
+ str << "<#{name} "
48
+ yield str
49
+ str << ">"
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,117 @@
1
+ require 'kangaroo/model/condition_normalizer'
2
+ require 'kangaroo/model/field'
3
+
4
+ module Kangaroo
5
+ module Model
6
+ module OpenObjectOrm
7
+ include ConditionNormalizer
8
+
9
+ # Search for records in the OpenERP database
10
+ #
11
+ # @param [Hash, Array, String] conditions
12
+ # @param [Hash] options
13
+ # @option options [Number] limit
14
+ # @option options [Number] offset
15
+ # @option options [String] order
16
+ # @option options [Hash] context
17
+ # @option options [boolean] count
18
+ # @return list of ids
19
+ def search conditions, options = {}
20
+ options = {
21
+ :limit => false,
22
+ :offset => 0
23
+ }.merge(options)
24
+
25
+ remote.search normalize_conditions(conditions),
26
+ options[:offset],
27
+ options[:limit],
28
+ options[:order],
29
+ options[:context],
30
+ options[:count]
31
+ end
32
+
33
+ # Read records and instantiate them
34
+ #
35
+ # @param [Array] ids
36
+ # @param [Hash] options
37
+ # @option options [Array] fields
38
+ # @option options [Hash] context
39
+ # @return [Array] list of Kangaroo::Model::Base instances
40
+ def read ids, options = {}
41
+ fields = options[:fields]
42
+ fields = attribute_names if options[:fields].blank?
43
+ context = options[:context]
44
+
45
+ [].tap do |result|
46
+ remote.read(ids, fields, context).each do |attributes|
47
+ position = ids.index(attributes[:id].to_i)
48
+ result[position] = instantiate attributes, context
49
+ end
50
+ end
51
+ end
52
+
53
+ # Retrieve field informations
54
+ #
55
+ # @param [Hash] options
56
+ # @option options [Array] fields
57
+ # @option options [Hash] context
58
+ # @return [Hash]
59
+ def fields_get options = {}
60
+ options = {
61
+ :fields => attribute_names,
62
+ :context => {}
63
+ }.merge(options)
64
+
65
+ remote.fields_get(options[:fields], options[:context]).map do |key, val|
66
+ Field.new key, val
67
+ end
68
+ end
69
+
70
+ # Create a OpenObject record.
71
+ #
72
+ # @param [Hash] attributes
73
+ # @param [Hash] options
74
+ # @option options [Hash] context
75
+ # @return [Number] id
76
+ def create_record attributes, options = {}
77
+ remote.create attributes, options[:context]
78
+ end
79
+
80
+
81
+ # Fetch default attribute values from OpenERP database
82
+ #
83
+ # @param [Hash] options
84
+ # @option options [Array] fields
85
+ # @option options [Hash] context
86
+ # @return [Hash] default values
87
+ def default_get options = {}
88
+ options = {
89
+ :fields => attribute_names
90
+ }.merge(options)
91
+
92
+ remote.default_get options[:fields], options[:context]
93
+ end
94
+
95
+ # Write values to records
96
+ #
97
+ # @param [Array] ids
98
+ # @param [Hash] attributes
99
+ # @param [Hash] options
100
+ # @option options [Hash] context
101
+ # @return [boolean] true/false
102
+ def write_record ids, attributes, options = {}
103
+ remote.write ids, attributes, options[:context]
104
+ end
105
+
106
+ # Remove records
107
+ #
108
+ # @param [Array] ids
109
+ # @param [Hash] options
110
+ # @option options [Hash] context
111
+ # @return [boolean] true/false
112
+ def unlink ids, options = {}
113
+ remote.unlink ids, options[:context]
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,180 @@
1
+ require 'kangaroo/exception'
2
+ require 'active_support/core_ext/hash'
3
+
4
+ module Kangaroo
5
+ module Model
6
+ module Persistence
7
+ # @private
8
+ def self.included klass
9
+ klass.extend ClassMethods
10
+ klass.define_model_callbacks :destroy, :save, :update, :create, :find
11
+ klass.after_destroy :mark_destroyed
12
+ klass.after_create :mark_persisted
13
+ klass.after_save :reload
14
+
15
+ klass.send :attr_accessor, :context
16
+
17
+ klass.before_initialize do
18
+ @destroyed = false
19
+ @readonly = false
20
+ @new_record = !@id
21
+ end
22
+ end
23
+
24
+ # Check if this record hasnt been persisted yet
25
+ #
26
+ # @return [boolean] true/false
27
+ def new_record?
28
+ @new_record
29
+ end
30
+
31
+ # Check if this record has been persisted yet
32
+ #
33
+ # @return [boolean] true/false
34
+ def persisted?
35
+ !@new_record
36
+ end
37
+
38
+ # Check if this record has been destroyed
39
+ #
40
+ # @return [boolean] true/false
41
+ def destroyed?
42
+ @destroyed
43
+ end
44
+
45
+ # Destroy this record
46
+ #
47
+ # @return self
48
+ def destroy
49
+ return true if destroyed? || new_record?
50
+ _run_destroy_callbacks do
51
+ remote.unlink [id], :context => context
52
+ end
53
+
54
+ self
55
+ end
56
+
57
+ # Save this record
58
+ #
59
+ # @param [Hash] options unused
60
+ # @return [boolean] true/false
61
+ def save options = {}
62
+ _run_save_callbacks do
63
+ create_or_update
64
+ end
65
+ end
66
+
67
+ # Save this record or raise an error
68
+ #
69
+ # @param [Hash] options unused
70
+ # @return [boolean] true
71
+ def save! options = {}
72
+ save options ||
73
+ raise(RecordSavingFailed)
74
+ end
75
+
76
+ # Reload this record, or just a subset of fields
77
+ #
78
+ # @param [Array] fields to reload
79
+ # @return self
80
+ def reload fields = self.class.attribute_names
81
+ @attributes = remote.read([id], fields, context).first.except(:id).stringify_keys
82
+ fields.each do |field|
83
+ @changed_attributes.delete field.to_s
84
+ end
85
+
86
+ self
87
+ end
88
+
89
+ module ClassMethods
90
+ # Initialize a record and immediately save it
91
+ #
92
+ # @param [Hash] attributes
93
+ # @return saved record
94
+ def create attributes = {}
95
+ new(attributes).tap do |new_record|
96
+ new_record.save
97
+ end
98
+ end
99
+
100
+ # Retrieve a record by id
101
+ #
102
+ # @param [Number] id
103
+ # @return record
104
+ # @raise RecordNotFound
105
+ def find id
106
+ Array === id ?
107
+ find_every(ids) :
108
+ find_single(id)
109
+ end
110
+
111
+ protected
112
+ def find_single id
113
+ read([id]).first ||
114
+ raise(RecordNotFound)
115
+ end
116
+
117
+ def find_every ids
118
+ read ids
119
+ end
120
+
121
+ def instantiate attributes, context = {}
122
+ allocate.tap do |instance|
123
+ instance.instance_exec(attributes.stringify_keys, context) do |attributes, context|
124
+ @attributes = attributes.except 'id'
125
+ @id = attributes['id']
126
+ @context = context
127
+ raise InstantiatedRecordNeedsIDError if @id.nil?
128
+
129
+ @new_record = false
130
+ @destroyed = false
131
+ @readonly = false
132
+
133
+ _run_initialize_callbacks
134
+ _run_find_callbacks
135
+ end
136
+ end
137
+ end
138
+ end
139
+
140
+ protected
141
+ def attributes_for_update
142
+ @attributes
143
+ end
144
+
145
+ def attributes_for_create
146
+ @attributes
147
+ end
148
+
149
+ def create_or_update
150
+ new_record? ? create : update
151
+ end
152
+
153
+ private
154
+ def create
155
+ _run_create_callbacks do
156
+ if id = self.class.create_record(attributes_for_create, :context => context)
157
+ @id = id
158
+ else
159
+ false
160
+ end
161
+ end
162
+ end
163
+
164
+ def update
165
+ _run_update_callbacks do
166
+ self.class.write_record [id], attributes_for_update, :context => context
167
+ end
168
+ end
169
+
170
+ def mark_persisted
171
+ @new_record = false
172
+ end
173
+
174
+ def mark_destroyed
175
+ @destroyed = true
176
+ freeze
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,212 @@
1
+ module Kangaroo
2
+ module Model
3
+ class Relation
4
+ # @private
5
+ ARRAY_DELEGATES = %w( all? any? as_json at b64encode blank? choice class clone collect collect! combination compact compact! concat
6
+ cycle decode64 delete delete_at delete_if detect drop drop_while dup duplicable? each each_cons each_index
7
+ each_slice each_with_index each_with_object empty? encode64 encode_json entries enum_cons enum_for enum_slice
8
+ enum_with_index eql? equal? exclude? extract_options! fetch fifth fill find_all find_index flatten
9
+ flatten! forty_two fourth freeze frozen? grep group_by html_safe? in_groups in_groups_of include? index
10
+ index_by indexes indices inject insert inspect instance_eval instance_exec instance_of? is_a? join kind_of?
11
+ last many? map map! max max_by member? min min_by minmax minmax_by nitems none? one? pack paginate
12
+ partition permutation pop presence present? pretty_inspect pretty_print pretty_print_cycle pretty_print_inspect
13
+ pretty_print_instance_variables product push rassoc reduce reject reject! replace respond_to? returning reverse
14
+ reverse! reverse_each rindex sample second shelljoin shift shuffle shuffle! slice slice! sort sort!
15
+ sort_by split sum take take_while tap third to to_ary to_default_s to_enum to_formatted_s to_json to_matcher
16
+ to_param to_query to_s to_sentence to_set to_xml to_xml_rpc to_yaml to_yaml_properties to_yaml_style transpose
17
+ type uniq uniq! uniq_by uniq_by! unshift values_at yaml_initialize zip |).freeze
18
+
19
+ # @private
20
+ attr_accessor :target, :where_clauses, :offset_clause, :limit_clause, :select_clause, :order_clause, :context_clause
21
+
22
+ alias_method :_clone, :clone
23
+ alias_method :_tap, :tap
24
+
25
+ delegate *(ARRAY_DELEGATES + [:to => :to_a])
26
+
27
+ # @private
28
+ def initialize target
29
+ @target = target
30
+ @where_clauses = []
31
+ @select_clause = []
32
+ @order_clause = []
33
+ @context_clause = {}
34
+ end
35
+
36
+ # Submit the query
37
+ def to_a
38
+ @target.search_and_read @where_clauses, search_parameters, read_parameters
39
+ end
40
+ alias_method :all, :to_a
41
+
42
+ # Return only the first record
43
+ def first
44
+ limit(1).to_a.first
45
+ end
46
+
47
+ # Return only the last record
48
+ def last
49
+ reverse.first
50
+ end
51
+
52
+ # Check if a record with fulfilling this conditions exist
53
+ def exists?
54
+ @target.search(@where_clauses, search_parameters.merge(:limit => 1)).present?
55
+ end
56
+
57
+ # Find record(s) by id(s)
58
+ def find ids
59
+ records = where(:id => ids)
60
+
61
+ Array === ids ?
62
+ records.all :
63
+ records.first
64
+ end
65
+
66
+ # Count how many records fulfill this conditions
67
+ def count
68
+ @target.count_by @where_clauses, search_parameters
69
+ end
70
+ alias_method :size, :count
71
+ alias_method :length, :count
72
+
73
+ # Reverse all order clauses
74
+ def reverse
75
+ if @order_clause.blank?
76
+ order('id', true)
77
+ else
78
+ _clone._tap do |c|
79
+ c.order_clause = c.order_clause.map do |order|
80
+ reverse_order order
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ # Clone this relation and add the condition to the where clause
87
+ #
88
+ # @param [Hash, Array, String] condition
89
+ # @return [Relation] cloned relation
90
+ def where condition
91
+ _clone._tap do |c|
92
+ c.where_clauses += [condition]
93
+ end
94
+ end
95
+
96
+ # Clone this relation and (re)set the limit clause
97
+ #
98
+ # @param [Number] limit maximum records to retriebe
99
+ # @return [Relation] cloned relation
100
+ def limit limit
101
+ _clone._tap do |c|
102
+ c.limit_clause = limit
103
+ end
104
+ end
105
+
106
+ # Clone this relation and (re)set the offset clause
107
+ #
108
+ # @param [Number] offset number of records to skip
109
+ # @return [Relation] cloned relation
110
+ def offset offset
111
+ _clone._tap do |c|
112
+ c.offset_clause = offset
113
+ end
114
+ end
115
+
116
+ # Clone this relation and add select expressions to select clause
117
+ #
118
+ # @param [Array, String, Symbol] selects fields to retrieve
119
+ # @return [Relation] cloned relation
120
+ def select *selects
121
+ selects = selects.flatten.map &:to_s
122
+ _clone._tap do |c|
123
+ c.select_clause += selects
124
+ end
125
+ end
126
+
127
+ # Clone this relation and add a context
128
+ #
129
+ # @param [Hash] context
130
+ # @return [Relation] cloned relation
131
+ def context context = {}
132
+ _clone._tap do |c|
133
+ c.context_clause = c.context_clause.merge(context)
134
+ end
135
+ end
136
+
137
+ # Clone this relation and add an order instruction
138
+ #
139
+ # @param [String, Symbol] column field to order by
140
+ # @param [boolean] desc true to order descending
141
+ def order column, desc = false
142
+ column = column.to_s + " desc" if desc
143
+ _clone._tap do |c|
144
+ c.order_clause += [column.to_s]
145
+ end
146
+ end
147
+
148
+ # Shortcut to set limit and offset and execute queries immediately.
149
+ # If {#limit} or {#offset} are already set, [] is equal to #to_a[]
150
+ #
151
+ # @overload [](n)
152
+ # @param [Number] n
153
+ # @return [Kangaroo::Model::Base] n-th record
154
+ # @overload [](n, m)
155
+ # @param [Number] n
156
+ # @param [Number] m
157
+ # @return [Array<Kangaroo::Model::Base>] m records, offset n
158
+ # @overload [](n..m)
159
+ # @param [Range] range n..m
160
+ # @return [Array<Kangaroo::Model::Base>] records m to n
161
+ def [] start_or_range, stop = nil
162
+ if @limit_clause || @offset_clause
163
+ return to_a[start_or_range, stop] if stop
164
+ return to_a[start_or_range]
165
+ end
166
+
167
+ c = _clone
168
+
169
+ c.offset_clause = if start_or_range.is_a?(Range)
170
+ range_end = start_or_range.end
171
+ range_end += 1 unless start_or_range.exclude_end?
172
+
173
+ c.limit_clause = range_end - start_or_range.begin
174
+ start_or_range.begin
175
+ elsif stop
176
+ c.limit_clause = stop
177
+ start_or_range
178
+ else
179
+ c.limit_clause = 1
180
+ start_or_range
181
+ end
182
+
183
+ (stop.nil? && Integer===start_or_range) ? c.to_a.first : c.to_a
184
+ end
185
+
186
+ protected
187
+ def search_parameters
188
+ {
189
+ :offset => @offset_clause,
190
+ :limit => @limit_clause,
191
+ :order => @order_clause.join(", "),
192
+ :context => @context_clause
193
+ }
194
+ end
195
+
196
+ def read_parameters
197
+ {
198
+ :fields => @select_clause,
199
+ :context => @context_clause
200
+ }
201
+ end
202
+
203
+ def reverse_order order
204
+ if match = order.match(/(.*)\sdesc/i)
205
+ match[1]
206
+ else
207
+ order + " desc"
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end