datapathy 0.6.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.
- data/.document +5 -0
- data/.gitignore +7 -0
- data/.rspec +3 -0
- data/Gemfile +6 -0
- data/LICENSE +20 -0
- data/README.mkd +19 -0
- data/Rakefile +40 -0
- data/VERSION +1 -0
- data/benchmarks/attributes.rb +67 -0
- data/benchmarks/initialize.rb +106 -0
- data/benchmarks/query.rb +34 -0
- data/datapathy.gemspec +31 -0
- data/lib/datapathy.rb +37 -0
- data/lib/datapathy/adapters/http_adapter.rb +121 -0
- data/lib/datapathy/collection.rb +123 -0
- data/lib/datapathy/config.rb +0 -0
- data/lib/datapathy/log_subscriber.rb +28 -0
- data/lib/datapathy/model.rb +142 -0
- data/lib/datapathy/model/crud.rb +61 -0
- data/lib/datapathy/model/dynamic_finders.rb +89 -0
- data/lib/datapathy/models/service.rb +6 -0
- data/lib/datapathy/query.rb +141 -0
- data/lib/datapathy/railtie.rb +48 -0
- data/profile/initialize.calltree +62 -0
- data/profile/initialize.rb +28 -0
- data/spec/adapter_api_spec.rb +146 -0
- data/spec/create_spec.rb +71 -0
- data/spec/crud_spec.rb +130 -0
- data/spec/datapathy_spec.rb +20 -0
- data/spec/delete_spec.rb +52 -0
- data/spec/find_by_spec.rb +24 -0
- data/spec/find_or_create_spec.rb +38 -0
- data/spec/integration/service_spec.rb +22 -0
- data/spec/query_spec.rb +89 -0
- data/spec/read_spec.rb +67 -0
- data/spec/spec_helper.rb +30 -0
- data/spec/support/datapathy_test_app.rb +33 -0
- data/spec/support/matchers.rb +30 -0
- data/spec/support/models.rb +25 -0
- data/spec/update_spec.rb +55 -0
- data/spec/validations_spec.rb +31 -0
- metadata +235 -0
@@ -0,0 +1,141 @@
|
|
1
|
+
require 'active_support/basic_object'
|
2
|
+
require 'active_support/core_ext/module/delegation'
|
3
|
+
|
4
|
+
class Datapathy::Query
|
5
|
+
|
6
|
+
attr_reader :model, :conditions,
|
7
|
+
:offset, :count, :instrumenter
|
8
|
+
|
9
|
+
def initialize(model, conditions = {}, &blk)
|
10
|
+
@model = model
|
11
|
+
@conditions = ConditionSet.new
|
12
|
+
@blocks = []
|
13
|
+
@instrumenter = ActiveSupport::Notifications.instrumenter
|
14
|
+
add(conditions, &blk)
|
15
|
+
end
|
16
|
+
|
17
|
+
def add(conditions = {}, &blk)
|
18
|
+
add_conditions_hash(conditions)
|
19
|
+
add_conditions(&blk) if block_given?
|
20
|
+
end
|
21
|
+
|
22
|
+
def add_conditions(&blk)
|
23
|
+
@blocks << blk
|
24
|
+
yield @conditions
|
25
|
+
end
|
26
|
+
|
27
|
+
def add_conditions_hash(conditions = {})
|
28
|
+
conditions.each do |k,v|
|
29
|
+
add_conditions { |q| q.send(k) == v }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def key_lookup?
|
34
|
+
@conditions.size == 1 &&
|
35
|
+
(@conditions.first.operation == :key || @conditions.first.operation == model.key) &&
|
36
|
+
@conditions.first.then.operation == :==
|
37
|
+
end
|
38
|
+
|
39
|
+
def key
|
40
|
+
@conditions.first.then.arguments.first
|
41
|
+
end
|
42
|
+
|
43
|
+
def initialize_and_filter(records)
|
44
|
+
filter(initialize_resources(records))
|
45
|
+
end
|
46
|
+
|
47
|
+
def initialize_resources(records)
|
48
|
+
return records if records.first.is_a?(Datapathy::Model)
|
49
|
+
records.map { |record|
|
50
|
+
resource = model.new(record)
|
51
|
+
resource.new_record = false
|
52
|
+
resource
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
56
|
+
def filter(resources)
|
57
|
+
resources = match_resources(resources)
|
58
|
+
resources = order_resources(resources)
|
59
|
+
resources = limit_resources(resources)
|
60
|
+
|
61
|
+
resources
|
62
|
+
end
|
63
|
+
|
64
|
+
def match_resources(resources)
|
65
|
+
resources.select do |record|
|
66
|
+
@blocks.all? do |block|
|
67
|
+
block.call(record)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def order_resources(resources)
|
73
|
+
resources
|
74
|
+
end
|
75
|
+
|
76
|
+
def limit_resources(resources)
|
77
|
+
return resources unless @offset || @count
|
78
|
+
resources.slice(@offset || 0, @count)
|
79
|
+
end
|
80
|
+
|
81
|
+
def limit(count, offset = 0)
|
82
|
+
@count, @offset = count, offset
|
83
|
+
end
|
84
|
+
|
85
|
+
def to_s
|
86
|
+
string = ""
|
87
|
+
string << @conditions.inspect
|
88
|
+
string << " limit #{@limit}" if @limit
|
89
|
+
string << " offset #{@offset}" if @offset && @offset > 0
|
90
|
+
string
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
class ConditionSet < ActiveSupport::BasicObject
|
95
|
+
|
96
|
+
delegate :size, :first, :to => :@conditions
|
97
|
+
|
98
|
+
def initialize
|
99
|
+
@conditions = []
|
100
|
+
end
|
101
|
+
|
102
|
+
def method_missing(method_name, *args, &blk)
|
103
|
+
condition = Condition.new(method_name, *args, &blk)
|
104
|
+
@conditions << condition
|
105
|
+
condition
|
106
|
+
end
|
107
|
+
|
108
|
+
def inspect
|
109
|
+
@conditions.inspect
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
class Condition < ActiveSupport::BasicObject
|
114
|
+
|
115
|
+
attr_reader :then
|
116
|
+
attr_accessor :operation, :arguments, :block
|
117
|
+
|
118
|
+
def initialize(operation, *arguments, &block)
|
119
|
+
@operation = operation
|
120
|
+
@arguments = arguments unless arguments.empty?
|
121
|
+
@block = block if block
|
122
|
+
end
|
123
|
+
|
124
|
+
def method_missing(method_name, *args, &blk)
|
125
|
+
@then = Condition.new(method_name, *args, &blk)
|
126
|
+
end
|
127
|
+
|
128
|
+
def respond_to?(arg)
|
129
|
+
false
|
130
|
+
end
|
131
|
+
|
132
|
+
def inspect
|
133
|
+
string = operation.to_s
|
134
|
+
string << "#{@arguments.inspect}" unless @arguments.nil?
|
135
|
+
string << " #{@then.inspect}" if @then
|
136
|
+
string
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'rails'
|
2
|
+
require 'active_model/railtie'
|
3
|
+
|
4
|
+
require 'action_controller/railtie'
|
5
|
+
|
6
|
+
module Datapathy
|
7
|
+
class Railtie < Rails::Railtie
|
8
|
+
|
9
|
+
initializer "datapathy.log_runtime" do |app|
|
10
|
+
ActiveSupport.on_load(:action_controller) do
|
11
|
+
include Datapathy::Railties::ControllerRuntime
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
require 'active_support/core_ext/module/attr_internal'
|
17
|
+
module Railties
|
18
|
+
module ControllerRuntime
|
19
|
+
extend ActiveSupport::Concern
|
20
|
+
|
21
|
+
attr_internal :query_runtime
|
22
|
+
|
23
|
+
def cleanup_view_runtime
|
24
|
+
runtime_before_render = Datapathy::LogSubscriber.reset_runtime
|
25
|
+
runtime = super
|
26
|
+
runtime_after_render = Datapathy::LogSubscriber.reset_runtime
|
27
|
+
self.query_runtime = runtime_before_render + runtime_after_render
|
28
|
+
runtime - runtime_after_render
|
29
|
+
end
|
30
|
+
|
31
|
+
def append_info_to_payload(payload)
|
32
|
+
super
|
33
|
+
payload[:query_runtime] = self.query_runtime
|
34
|
+
end
|
35
|
+
|
36
|
+
module ClassMethods
|
37
|
+
def log_process_action(payload)
|
38
|
+
messages, query_runtime = super, payload[:query_runtime]
|
39
|
+
messages << ("Datapathy: %.1fms" % query_runtime.to_f) if query_runtime
|
40
|
+
messages
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
@@ -0,0 +1,62 @@
|
|
1
|
+
events: process_time
|
2
|
+
|
3
|
+
fl=/home/rando/development/api/datapathy/(eval)
|
4
|
+
fn=DatapathyModel::text=
|
5
|
+
5 110000
|
6
|
+
|
7
|
+
fl=/home/rando/development/api/datapathy/(eval)
|
8
|
+
fn=DatapathyModel::title=
|
9
|
+
5 60000
|
10
|
+
|
11
|
+
fl=/home/rando/development/api/datapathy/(eval)
|
12
|
+
fn=DatapathyModel::id=
|
13
|
+
5 80000
|
14
|
+
|
15
|
+
fl=/home/rando/development/api/datapathy/lib/datapathy/model.rb
|
16
|
+
fn=Datapathy::Model::initialize
|
17
|
+
8 390000
|
18
|
+
cfl=/home/rando/development/api/datapathy/(eval)
|
19
|
+
cfn=DatapathyModel::id=
|
20
|
+
calls=100000 12
|
21
|
+
12 80000
|
22
|
+
cfl=/home/rando/development/api/datapathy/(eval)
|
23
|
+
cfn=DatapathyModel::title=
|
24
|
+
calls=100000 13
|
25
|
+
13 60000
|
26
|
+
cfl=/home/rando/development/api/datapathy/(eval)
|
27
|
+
cfn=DatapathyModel::text=
|
28
|
+
calls=100000 14
|
29
|
+
14 110000
|
30
|
+
|
31
|
+
fl=/home/rando/development/api/datapathy/ruby_runtime
|
32
|
+
fn=<Class::BasicObject>::allocate
|
33
|
+
0 50000
|
34
|
+
|
35
|
+
fl=/home/rando/development/api/datapathy/ruby_runtime
|
36
|
+
fn=Class::new
|
37
|
+
0 120000
|
38
|
+
cfl=/home/rando/development/api/datapathy/ruby_runtime
|
39
|
+
cfn=<Class::BasicObject>::allocate
|
40
|
+
calls=100000 19
|
41
|
+
19 50000
|
42
|
+
cfl=/home/rando/development/api/datapathy/lib/datapathy/model.rb
|
43
|
+
cfn=Datapathy::Model::initialize
|
44
|
+
calls=100000 19
|
45
|
+
19 640000
|
46
|
+
|
47
|
+
fl=/home/rando/development/api/datapathy/ruby_runtime
|
48
|
+
fn=Integer::times
|
49
|
+
0 80000
|
50
|
+
cfl=/home/rando/development/api/datapathy/ruby_runtime
|
51
|
+
cfn=Class::new
|
52
|
+
calls=100000 19
|
53
|
+
19 810000
|
54
|
+
|
55
|
+
fl=/home/rando/development/api/datapathy/profile/initialize.rb
|
56
|
+
fn=Global::[No method]
|
57
|
+
18 0
|
58
|
+
cfl=/home/rando/development/api/datapathy/ruby_runtime
|
59
|
+
cfn=Integer::times
|
60
|
+
calls=1 18
|
61
|
+
18 890000
|
62
|
+
|
@@ -0,0 +1,28 @@
|
|
1
|
+
|
2
|
+
require 'rubygems'
|
3
|
+
|
4
|
+
$LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + "/../lib"))
|
5
|
+
require 'datapathy'
|
6
|
+
class DatapathyModel
|
7
|
+
include Datapathy::Model
|
8
|
+
|
9
|
+
persists :id, :title, :text
|
10
|
+
end
|
11
|
+
|
12
|
+
ATTRS = {:id => 1, :title => "Foo", :text => "Bar"}
|
13
|
+
|
14
|
+
require 'ruby-prof'
|
15
|
+
|
16
|
+
# Profile the code
|
17
|
+
result = RubyProf.profile do
|
18
|
+
100_000.times do
|
19
|
+
DatapathyModel.new(ATTRS)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Print a graph profile to text
|
24
|
+
printer = RubyProf::CallTreePrinter.new(result)
|
25
|
+
File.open(File.dirname(__FILE__) + "/" + File.basename(__FILE__, ".rb") + ".calltree", "w") do |file|
|
26
|
+
printer.print(file, 0)
|
27
|
+
end
|
28
|
+
|
@@ -0,0 +1,146 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
describe 'Adapter API' do
|
4
|
+
|
5
|
+
share_as :ArrayOfHashes do
|
6
|
+
it 'should return an Array(Hash) of records' do
|
7
|
+
@results.should be_an(Array)
|
8
|
+
@results.should have(1).item
|
9
|
+
|
10
|
+
result = @results.first
|
11
|
+
result.should be_a(Hash)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
before do
|
17
|
+
@adapter = Datapathy::Adapters::MemoryAdapter.new
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "#create(collection)" do
|
21
|
+
before do
|
22
|
+
@article = Article.new(:id => new_uuid,
|
23
|
+
:title => "Whee!",
|
24
|
+
:text => "This is fun!")
|
25
|
+
@collection = Datapathy::Collection.new(@article)
|
26
|
+
|
27
|
+
@results = @adapter.create(@collection)
|
28
|
+
@result = @results.first
|
29
|
+
end
|
30
|
+
|
31
|
+
it_should_behave_like ArrayOfHashes
|
32
|
+
|
33
|
+
it 'should return the populated attributes' do
|
34
|
+
@result.should == @article.persisted_attributes
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe "#read(query)" do
|
39
|
+
before do
|
40
|
+
@article = Article.new(:id => new_uuid,
|
41
|
+
:title => "Whee!",
|
42
|
+
:text => "This is fun!")
|
43
|
+
|
44
|
+
@adapter.datastore[Article][@article.id] = @article.persisted_attributes
|
45
|
+
|
46
|
+
@query = Datapathy::Query.new(Article) { |q| q.title == @article.title }
|
47
|
+
@collection = Datapathy::Collection.new(@query)
|
48
|
+
|
49
|
+
@results = @adapter.read(@collection)
|
50
|
+
@result = @results.first
|
51
|
+
end
|
52
|
+
|
53
|
+
it_should_behave_like ArrayOfHashes
|
54
|
+
|
55
|
+
it 'should have the attributes' do
|
56
|
+
@result.should == @article.persisted_attributes
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe "#update(attributes, collection)" do
|
61
|
+
before do
|
62
|
+
@article = Article.new(:id => new_uuid,
|
63
|
+
:title => "Whee!",
|
64
|
+
:text => "This is fun!")
|
65
|
+
|
66
|
+
@adapter.datastore[Article][@article.id] = @article.persisted_attributes
|
67
|
+
end
|
68
|
+
|
69
|
+
describe "with loaded collection (update already retrived records)" do
|
70
|
+
before do
|
71
|
+
@collection = Datapathy::Collection.new(@article)
|
72
|
+
@collection.should be_loaded
|
73
|
+
@results = @adapter.update({:title => "Boo"}, @collection)
|
74
|
+
@result = @results.first
|
75
|
+
end
|
76
|
+
|
77
|
+
it_should_behave_like ArrayOfHashes
|
78
|
+
|
79
|
+
it 'should return the update attribute values' do
|
80
|
+
@result[:title].should == "Boo"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe "with empty collection (update in bulk, without retrieval)" do
|
85
|
+
before do
|
86
|
+
@query = Datapathy::Query.new(Article) { |q| q.title == @article.title }
|
87
|
+
@collection = Datapathy::Collection.new(@query)
|
88
|
+
@collection.should_not be_loaded
|
89
|
+
|
90
|
+
@results = @adapter.update({:title => "Boo"}, @collection)
|
91
|
+
@result = @results.first
|
92
|
+
end
|
93
|
+
|
94
|
+
it_should_behave_like ArrayOfHashes
|
95
|
+
|
96
|
+
it 'should return an updated attribute values' do
|
97
|
+
@result[:title].should == "Boo"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
|
103
|
+
describe "#delete(attributes)" do
|
104
|
+
before do
|
105
|
+
@article = Article.new(:id => new_uuid,
|
106
|
+
:title => "Whee!",
|
107
|
+
:text => "This is fun!")
|
108
|
+
|
109
|
+
@adapter.datastore[Article][@article.id] = @article.persisted_attributes
|
110
|
+
end
|
111
|
+
|
112
|
+
describe "with loaded collection (delete already retrived records)" do
|
113
|
+
before do
|
114
|
+
@collection = Datapathy::Collection.new(@article)
|
115
|
+
@collection.should be_loaded
|
116
|
+
@results = @adapter.delete(@collection)
|
117
|
+
@result = @results.first
|
118
|
+
end
|
119
|
+
|
120
|
+
it_should_behave_like ArrayOfHashes
|
121
|
+
|
122
|
+
it 'should return the remove attributes' do
|
123
|
+
@result.should == @article.persisted_attributes
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
describe "with empty collection (delete in bulk, without retrieval)" do
|
128
|
+
before do
|
129
|
+
@query = Datapathy::Query.new(Article) { |q| q.title == @article.title }
|
130
|
+
@collection = Datapathy::Collection.new(@query)
|
131
|
+
@collection.should_not be_loaded
|
132
|
+
|
133
|
+
@results = @adapter.delete(@collection)
|
134
|
+
@result = @results.first
|
135
|
+
end
|
136
|
+
|
137
|
+
it_should_behave_like ArrayOfHashes
|
138
|
+
|
139
|
+
it 'should return the removed attribute values' do
|
140
|
+
@result.should == @article.persisted_attributes
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
data/spec/create_spec.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
describe "Creating models" do
|
4
|
+
|
5
|
+
share_as :CreatingARecord do
|
6
|
+
it 'should create a record' do
|
7
|
+
test_adapter.datastore[Article].should have(1).key
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'should store persistable attributes' do
|
11
|
+
record = test_adapter.datastore[Article][@article.id]
|
12
|
+
|
13
|
+
record[:id].should eql(@article.id)
|
14
|
+
record[:title].should eql(@article.title)
|
15
|
+
record[:text].should eql(@article.text)
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'should not store other attributes' do
|
19
|
+
record = test_adapter.datastore[Article][@article.id]
|
20
|
+
|
21
|
+
@article.should respond_to(:summary)
|
22
|
+
record.should_not have_key("summary")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe 'Model.create' do
|
27
|
+
before do
|
28
|
+
pending
|
29
|
+
@article = Article.create(:id => new_uuid,
|
30
|
+
:title => "Datapathy is awesome!",
|
31
|
+
:text => "It just is!")
|
32
|
+
end
|
33
|
+
|
34
|
+
it_should_behave_like CreatingARecord
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
describe 'Model.new; #save' do
|
39
|
+
before do
|
40
|
+
pending
|
41
|
+
@article = Article.new(:id => new_uuid,
|
42
|
+
:title => "Datapathy is awesome!",
|
43
|
+
:text => "It just is!")
|
44
|
+
@article.save
|
45
|
+
end
|
46
|
+
|
47
|
+
it_should_behave_like CreatingARecord
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
describe 'Bulk Model.create' do
|
52
|
+
before do
|
53
|
+
pending
|
54
|
+
@articles = Article.create([
|
55
|
+
{ :id => new_uuid, :title => "Article A", :text => "Foo"},
|
56
|
+
{ :id => new_uuid, :title => "Article B", :text => "Bar"},
|
57
|
+
{ :id => new_uuid, :title => "Article C", :text => "Baz"}
|
58
|
+
])
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'should create the records' do
|
62
|
+
test_adapter.datastore[Article].should have(@articles.size).keys
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
after do
|
68
|
+
test_adapter.clear!
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|