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.
@@ -0,0 +1,123 @@
1
+ class Datapathy::Collection
2
+
3
+ attr_reader :query, :model, :adapter
4
+
5
+ # Collection.new(query)
6
+ # Collection.new(model, ...)
7
+ # Collection.new(Model, record, ...)
8
+ def initialize(*elements)
9
+ if elements.first.is_a?(Datapathy::Query)
10
+ query = elements.shift
11
+ elsif elements.first.is_a?(Datapathy::Model)
12
+ query = Datapathy::Query.new(elements.first.model)
13
+ elsif elements.first.ancestors.include?(Datapathy::Model)
14
+ query = Datapathy::Query.new(elements.shift)
15
+ else
16
+ raise "First element must be a query, model, or Model class"
17
+ end
18
+
19
+ @query, @model, @adapter = query, query.model, query.model.adapter
20
+ @elements = elements.first.is_a?(Hash) ? Array.wrap(query.model.new(*elements)) : elements
21
+ end
22
+
23
+ def new(*attributes)
24
+ self.model.new(*attributes)
25
+ end
26
+
27
+ def detect(*attrs, &blk)
28
+ slice(0, 1)
29
+ select(*attrs, &blk)
30
+ to_a.first
31
+ end
32
+ alias find detect
33
+ alias first detect
34
+
35
+ def select(*attrs, &blk)
36
+ query.add(*attrs, &blk)
37
+ self
38
+ end
39
+ alias find_all select
40
+
41
+ def slice(index_or_start_or_range, length = nil)
42
+ if index_or_start_or_range.is_a?(Range)
43
+ range = index_or_start_or_range
44
+ count, offset = (range.last - range.first), range.first
45
+ elsif length
46
+ start = index_or_start_or_range
47
+ count, offset = length, start
48
+ else
49
+ count, offset = 1, index_or_start_or_range
50
+ end
51
+
52
+ query.limit(count, offset)
53
+ end
54
+
55
+ def create(*attributes)
56
+ query.instrumenter.instrument('query.datapathy', :name => "Create #{model.to_s}", :query => attributes.inspect) do
57
+ if attributes.empty?
58
+ adapter.create(self)
59
+ each { |r| r.new_record = false }
60
+ size == 1 ? first : self
61
+ else
62
+ self.class.new(query, *attributes).create
63
+ end
64
+ end
65
+ end
66
+
67
+ def update(attributes = {}, &blk)
68
+ query.add(&blk)
69
+ query.instrumenter.instrument('query.datapathy', :name => "Update #{model.to_s}", :query => attributes.inspect) do
70
+ @elements = query.initialize_resources(adapter.update(attributes, self))
71
+ end
72
+ end
73
+
74
+ def delete(&blk)
75
+ query.add(&blk)
76
+ query.instrumenter.instrument('query.datapathy', :name => "Delete #{model.to_s}", :query => query.to_s) do
77
+ @elements = query.initialize_resources(adapter.delete(self))
78
+ end
79
+ end
80
+
81
+ def loaded?
82
+ !@elements.empty?
83
+ end
84
+
85
+ # Since @elements is an array, pretty much every array method should trigger
86
+ # a load. The exceptions are the ones defined above.
87
+ TRIGGER_METHODS = (Array.instance_methods - self.instance_methods).freeze
88
+ TRIGGER_METHODS.each do |meth|
89
+ class_eval <<-EVAL, __FILE__, __LINE__
90
+ def #{meth}(*a, &b)
91
+ self.load! unless loaded?
92
+ @elements.#{meth}(*a, &b)
93
+ end
94
+ EVAL
95
+ end
96
+
97
+ def to_a
98
+ self.load! unless loaded?
99
+ @elements
100
+ end
101
+
102
+ def load!
103
+ query.instrumenter.instrument('query.datapathy', :name => "Read #{model.to_s}", :query => query.to_s) do
104
+ @elements = query.initialize_and_filter(adapter.read(self))
105
+ end
106
+ end
107
+
108
+ def to_sql(formatter = nil)
109
+ if any?
110
+ "(" + collect { |e| e.to_sql(formatter) }.join(', ') + ")"
111
+ else
112
+ "(NULL)"
113
+ end
114
+ end
115
+
116
+ def equality_predicate_sql
117
+ "IN"
118
+ end
119
+
120
+ def inequality_predicate_sql
121
+ "NOT IN"
122
+ end
123
+ end
File without changes
@@ -0,0 +1,28 @@
1
+ require 'active_support/log_subscriber'
2
+ require 'active_support/notifications'
3
+
4
+ module Datapathy
5
+ class LogSubscriber < ActiveSupport::LogSubscriber
6
+
7
+ def self.runtime=(value)
8
+ Thread.current["datapathy_query_runtime"] = value
9
+ end
10
+
11
+ def self.runtime
12
+ Thread.current["datapathy_query_runtime"] ||= 0
13
+ end
14
+
15
+ def self.reset_runtime
16
+ rt, self.runtime = runtime, 0
17
+ rt
18
+ end
19
+
20
+ def query(event)
21
+ self.class.runtime += event.duration
22
+ debug("Datapathy: %s (%.1fms) %s" % [event.payload[:name], event.duration, event.payload[:query]])
23
+ end
24
+
25
+ end
26
+ end
27
+
28
+ Datapathy::LogSubscriber.attach_to :datapathy
@@ -0,0 +1,142 @@
1
+ require 'active_support/concern'
2
+ require 'active_support/core_ext/class/inheritable_attributes'
3
+ require 'active_support/core_ext/hash/indifferent_access'
4
+ require 'active_support/core_ext/hash/slice'
5
+
6
+ require 'active_model'
7
+
8
+ require 'datapathy/query'
9
+
10
+ require 'datapathy/model/crud'
11
+ require 'datapathy/model/dynamic_finders'
12
+
13
+ module Datapathy::Model
14
+ extend ActiveSupport::Concern
15
+ extend ActiveModel::Naming
16
+
17
+ include ActiveModel::Conversion
18
+
19
+ include ActiveModel::Validations
20
+
21
+ include Datapathy::Model::Crud
22
+ include Datapathy::Model::DynamicFinders
23
+
24
+ attr_accessor :new_record
25
+
26
+ def initialize(attributes = {})
27
+ @attributes = {}
28
+ merge(attributes)
29
+ @new_record = true
30
+ end
31
+
32
+ def persisted_attributes
33
+ @attributes
34
+ end
35
+
36
+ def merge(attributes = {})
37
+ attributes.each do |name, value|
38
+ method = :"#{name}="
39
+ send(method, value) if respond_to?(method)
40
+ end
41
+ end
42
+
43
+ def merge!(attributes = {})
44
+ @attributes = @attributes || {}
45
+ @attributes.merge!(attributes)
46
+ end
47
+
48
+ def key
49
+ send(self.class.key)
50
+ end
51
+
52
+ def key=(value)
53
+ send(:"#{self.class.key}=", value)
54
+ end
55
+
56
+ def model
57
+ self.class
58
+ end
59
+
60
+ def ==(other)
61
+ self.key == (other && other.key)
62
+ end
63
+
64
+ def new_record?
65
+ @new_record
66
+ end
67
+
68
+ def adapter
69
+ self.class.adapter
70
+ end
71
+
72
+ #override the ActiveModel::Validations one, because its dumb
73
+ def valid?
74
+ _run_validate_callbacks if errors.empty?
75
+ errors.empty?
76
+ end
77
+
78
+ module ClassMethods
79
+
80
+ def new(*attributes)
81
+ attributes = [{}] if attributes.empty?
82
+ resources = attributes.map do |attrs|
83
+ super(attrs)
84
+ end
85
+
86
+ collection = Datapathy::Collection.new(*resources)
87
+ collection.size == 1 ? collection.first : collection
88
+ end
89
+
90
+ def persists(*args)
91
+ persisted_attributes.push(*args)
92
+ args.each do |name|
93
+ name = name.to_s.gsub(/\?\Z/, '')
94
+ define_getter_method(name)
95
+ define_setter_method(name)
96
+ end
97
+ end
98
+
99
+ def define_getter_method(name)
100
+ class_eval <<-CODE
101
+ def #{name}
102
+ @attributes[:#{name}]
103
+ end
104
+ alias #{name}? #{name}
105
+ CODE
106
+ end
107
+
108
+ def define_setter_method(name)
109
+ class_eval <<-CODE
110
+ def #{name}=(val)
111
+ @attributes[:#{name}] = val
112
+ end
113
+ CODE
114
+ end
115
+
116
+ def persisted_attributes
117
+ @persisted_attributes ||= []
118
+ end
119
+
120
+ def new_from_attributes(attributes = {})
121
+ m = allocate
122
+ m.merge!(attributes = {})
123
+ m.new_record = false
124
+ m
125
+ end
126
+
127
+ def key
128
+ :href
129
+ end
130
+
131
+ def adapter
132
+ Datapathy.adapter
133
+ end
134
+
135
+ def model
136
+ self
137
+ end
138
+
139
+ end
140
+
141
+ end
142
+
@@ -0,0 +1,61 @@
1
+
2
+ module Datapathy::Model
3
+ module Crud
4
+
5
+ extend ActiveSupport::Concern
6
+
7
+ def save
8
+ if new_record?
9
+ create()
10
+ else
11
+ update()
12
+ end
13
+ end
14
+
15
+ def create
16
+ Datapathy::Collection.new(self).create
17
+ end
18
+
19
+ def update
20
+ Datapathy::Collection.new(self).update(persisted_attributes).first
21
+ end
22
+
23
+ def delete
24
+ Datapathy::Collection.new(self).delete.first
25
+ end
26
+
27
+ module ClassMethods
28
+ def create(*attributes)
29
+ collection = Datapathy::Collection.new(self, *attributes).create
30
+ end
31
+
32
+ def [](value)
33
+ detect{ |m| m.key == value} || raise(Datapathy::RecordNotFound, "No #{model} found with #{key} `#{value}`")
34
+ end
35
+
36
+ def select(*attrs, &blk)
37
+ query = Datapathy::Query.new(model)
38
+ Datapathy::Collection.new(query).select(*attrs, &blk)
39
+ end
40
+ alias all select
41
+ alias find_all select
42
+
43
+ def detect(*attrs, &blk)
44
+ select(*attrs, &blk).first
45
+ end
46
+ alias first detect
47
+ alias find detect
48
+
49
+ def update(attributes, &blk)
50
+ select(&blk).update(attributes)
51
+ end
52
+
53
+ def delete(&blk)
54
+ select(&blk).delete
55
+ end
56
+
57
+ end
58
+
59
+ end
60
+
61
+ end
@@ -0,0 +1,89 @@
1
+
2
+ module Datapathy::Model
3
+ module DynamicFinders
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+
8
+ def method_missing(method_id, *arguments, &block)
9
+ if match = DynamicFinderMatch.match(method_id)
10
+ if match.finder?
11
+ define_find_by(method_id, match)
12
+ elsif match.instantiator?
13
+ define_find_or_create_by(method_id, match)
14
+ end
15
+ send(method_id, *arguments, &block)
16
+ else
17
+ super
18
+ end
19
+ end
20
+
21
+ def define_find_by(method_id, match)
22
+ self.class_eval %{
23
+ def self.#{method_id}(*args)
24
+ find_attributes = Hash[[:#{match.attribute_names.join(',:')}].zip(args)]
25
+
26
+ #{match.finder == :first ? "detect(find_attributes)" : "select(find_attributes)"}
27
+ end
28
+ }, __FILE__, __LINE__
29
+ end
30
+
31
+ def define_find_or_create_by(method_id, match)
32
+ self.class_eval %{
33
+ def self.#{method_id}(*args)
34
+ if args[0].is_a?(Hash)
35
+ attributes = args[0]
36
+ find_attributes = attributes.slice(*[:#{match.attribute_names.join(',:')}])
37
+ end
38
+
39
+ record = detect(find_attributes)
40
+
41
+ if record.nil?
42
+ record = self.new(attributes)
43
+ #{'record.save' if match.instantiator == :create}
44
+ end
45
+
46
+ record
47
+ end
48
+ }, __FILE__, __LINE__
49
+ end
50
+
51
+ class DynamicFinderMatch
52
+
53
+ attr_reader :finder, :attribute_names, :instantiator
54
+
55
+ def self.match(method)
56
+ df_match = self.new(method)
57
+ df_match.finder ? df_match : nil
58
+ end
59
+
60
+ def initialize(method)
61
+ @finder = :first
62
+ case method.to_s
63
+ when /^find_(all_by|last_by|by)_([_a-zA-Z]\w*)$/
64
+ @finder = :last if $1 == 'last_by'
65
+ @finder = :all if $1 == 'all_by'
66
+ names = $2
67
+ when /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/
68
+ @instantiator = $1 == 'initialize' ? :new : :create
69
+ names = $2
70
+ else
71
+ @finder = nil
72
+ end
73
+ @attribute_names = names && names.split('_and_')
74
+ end
75
+
76
+ def finder?
77
+ !@finder.nil? && @instantiator.nil?
78
+ end
79
+
80
+ def instantiator?
81
+ @finder == :first && !@instantiator.nil?
82
+ end
83
+
84
+ end
85
+
86
+ end
87
+
88
+ end
89
+ end