chawk 0.2.0 → 0.3.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/lib/node.rb ADDED
@@ -0,0 +1,313 @@
1
+ require 'active_record'
2
+ module Chawk
3
+ # Models used in Chawk. ActiveRecord classes.
4
+ module Models
5
+ class NodeInvalidator
6
+ extend Forwardable
7
+ def_delegators :sweeps, :size, :map
8
+
9
+ def initialize(node)
10
+ @node = node
11
+ @sweeps = []
12
+ @ranges = []
13
+ end
14
+
15
+ def <<(time)
16
+ @node.ranges.where("start_ts <= ? AND stop_ts >= ?",time,time).each do |range|
17
+ @ranges << range.id unless @ranges.include?(range.id)
18
+ end
19
+ end
20
+
21
+ def invalidate!()
22
+ @ranges.each{|r|Chawk::Models::Range.find(r).populate!}
23
+ end
24
+ end
25
+
26
+ class NodeAggregator
27
+
28
+ attr_reader :dataset
29
+
30
+ def initialize(node)
31
+ node.check_read_access
32
+ if node.points.length > 0
33
+ @dataset = node.points.to_a.reduce([]) {|ary,p| ary << p.value}
34
+ end
35
+ end
36
+
37
+ def max()
38
+ @dataset.max
39
+ end
40
+
41
+ def min()
42
+ @dataset.min
43
+ end
44
+
45
+ def mean
46
+ sum.to_f / @dataset.length
47
+ end
48
+
49
+ def sum
50
+ @dataset.reduce(0) {|sum,p| sum+=p}
51
+ end
52
+
53
+ def count
54
+ @dataset.length
55
+ end
56
+
57
+ def sumsqr
58
+ @dataset.map {|x| x * x}.reduce(&:+)
59
+ end
60
+
61
+ def stdev
62
+ m = mean
63
+ Math.sqrt((sumsqr - count * m * m)/(count-1))
64
+ end
65
+ end
66
+
67
+ # The Node, where most Chawk:Node information is persisted..
68
+ class Node < ActiveRecord::Base
69
+ attr_accessor :agent
70
+ after_initialize :init
71
+ self.table_name_prefix = "chawk_"
72
+ belongs_to :agent
73
+ has_many :points
74
+ has_many :values
75
+ has_many :relations
76
+ has_many :ranges, foreign_key: :parent_node_id
77
+
78
+ attr_accessor :access
79
+
80
+ def init
81
+ @agent = nil
82
+ end
83
+
84
+ def clear_values!
85
+ check_admin_access
86
+ values.destroy_all
87
+ end
88
+
89
+ def clear_points!
90
+ check_admin_access
91
+ points.destroy_all
92
+ end
93
+
94
+ def _prepare_insert(val, ts, options)
95
+ values = {value:val,observed_at:ts.to_f}
96
+ if options[:meta]
97
+ if options[:meta].is_a?(Hash)
98
+ values[:meta] = options[:meta].to_json
99
+ else
100
+ raise ArgumentError, "Meta must be a JSON-representable Hash. #{options[:meta].inspect}"
101
+ end
102
+ end
103
+ values
104
+ end
105
+
106
+ def _insert_value(val,ts,options={})
107
+ self.values.create(_prepare_insert(val, ts, options))
108
+ ts
109
+ end
110
+
111
+ def value_recognizer(item, dt, options={})
112
+ case
113
+ when item.is_a?(String)
114
+ _insert_value item,dt, options
115
+ else
116
+ raise ArgumentError, "Can't recognize format of data item. #{item.inspect}"
117
+ end
118
+ end
119
+
120
+ def _unravel(items)
121
+ if items.is_a?(Array)
122
+ items.each do |item|
123
+ yield item
124
+ end
125
+ else
126
+ yield items
127
+ end
128
+ end
129
+
130
+ def _add(args, type, options={})
131
+ check_write_access
132
+ ni = NodeInvalidator.new(self)
133
+ options[:observed_at] ? dt = options[:observed_at] : dt = Time.now
134
+ _unravel(args) do |arg|
135
+ case type
136
+ when :point
137
+ ni << point_recognizer(arg, dt, options)
138
+ when :value
139
+ ni << value_recognizer(arg, dt, options)
140
+ end
141
+ end
142
+ ni.invalidate!
143
+ end
144
+
145
+ # @param args [Object, Array of Objects]
146
+ # @param options [Hash] You can also pass in :meta and :timestamp
147
+ # Add an item or an array of items (one at a time) to the datastore.
148
+ def add_values(args,options={})
149
+ _add(args,:value, options)
150
+ end
151
+
152
+ # @param args [Object, Array of Objects]
153
+ # @param options [Hash] You can also pass in :meta and :timestamp
154
+ # Add an item or an array of items (one at a time) to the datastore.
155
+ def add_points(args,options={})
156
+ _add(args,:point,options)
157
+ end
158
+
159
+ def _insert_point(val,ts,options={})
160
+ self.points.create(_prepare_insert(val, ts, options))
161
+ ts
162
+ end
163
+
164
+ def _insert_point_hash(item,ts,options)
165
+ if item['v'] && item['v'].is_a?(Integer)
166
+ if item['t']
167
+ _insert_point item['v'],item['t'], options
168
+ else
169
+ _insert_point item['v'],ts, options
170
+ end
171
+ else
172
+ raise ArgumentError, "Hash must have 'v' key set to proper type.. #{item.inspect}"
173
+ end
174
+ end
175
+
176
+ def _insert_point_array(item,options)
177
+ if item.length == 2 && item[0].is_a?(Integer)
178
+ _insert_point item[0],item[1], options
179
+ else
180
+ raise ArgumentError, "Array Items must be in [value,timestamp] format. #{item.inspect}"
181
+ end
182
+ end
183
+
184
+ def _insert_point_string(item,ts,options)
185
+ if item.length > 0 && item =~ /\A[-+]?[0-9]+/
186
+ _insert_point item.to_i,ts, options
187
+ else
188
+ raise ArgumentError, "String Items must represent Integer. #{item.inspect}"
189
+ end
190
+ end
191
+
192
+ def point_recognizer(item, dt, options={})
193
+ case
194
+ when item.is_a?(Integer)
195
+ _insert_point item,dt, options
196
+ when item.is_a?(Array)
197
+ _insert_point_array(item, options)
198
+ when item.is_a?(Hash)
199
+ _insert_point_hash(item,dt,options)
200
+ when item.is_a?(String)
201
+ _insert_point_string(item,dt,options)
202
+ else
203
+ raise ArgumentError, "Can't recognize format of data item. #{item.inspect}"
204
+ end
205
+ end
206
+
207
+ def check_write_access
208
+ unless [:full,:admin,:write].include? @access
209
+ raise SecurityError,"You do not have write access to this node."
210
+ end
211
+ end
212
+
213
+ def check_read_access
214
+ unless [:full,:admin,:read].include? @access
215
+ raise SecurityError,"You do not have read access to this node."
216
+ end
217
+ end
218
+
219
+ def check_admin_access
220
+ unless [:full,:admin,:read].include? @access
221
+ raise SecurityError,"You do not have admin access to this node."
222
+ end
223
+ end
224
+
225
+ def increment(value=1, options={})
226
+ check_write_access
227
+ if value.is_a?(Integer)
228
+ last = self.points.last
229
+ add_points last.value + value,options
230
+ else
231
+ raise ArgumentError, "Value must be an Integer"
232
+ end
233
+ end
234
+
235
+ def decrement(value=1, options={})
236
+ check_write_access
237
+ if value.is_a?(Integer)
238
+ increment (-1) * value, options
239
+ else
240
+ raise ArgumentError, "Value must be an Integer"
241
+ end
242
+ end
243
+
244
+ def _range(dt_from, dt_to, coll, options={})
245
+ check_read_access
246
+ ret = coll.where("observed_at >= :dt_from AND observed_at <= :dt_to",{dt_from:dt_from.to_f,dt_to:dt_to.to_f}, limit:1000,order:"observed_at asc, id asc")
247
+ end
248
+
249
+ # Returns items whose observed_at times fit within from a range.
250
+ # @param dt_from [Time::Time] The start time.
251
+ # @param dt_to [Time::Time] The end time.
252
+ # @return [Array of Objects]
253
+ def values_range(dt_from, dt_to,options={})
254
+ _range(dt_from, dt_to, values, options)
255
+ end
256
+
257
+ # Returns items whose observed_at times fit within from a range ending now.
258
+ # @param dt_from [Time::Time] The start time.
259
+ # @return [Array of Objects]
260
+ def values_since(dt_from)
261
+ self.values_range(dt_from,Time.now)
262
+ end
263
+
264
+ # Returns items whose observed_at times fit within from a range.
265
+ # @param dt_from [Time::Time] The start time.
266
+ # @param dt_to [Time::Time] The end time.
267
+ # @return [Array of Objects]
268
+ def points_range(dt_from, dt_to,options={})
269
+ _range(dt_from, dt_to, points, options)
270
+ end
271
+
272
+ # Returns items whose observed_at times fit within from a range ending now.
273
+ # @param dt_from [Time::Time] The start time.
274
+ # @return [Array of Objects]
275
+ def points_since(dt_from)
276
+ self.points_range(dt_from,Time.now)
277
+ end
278
+
279
+ # Sets public read flag for this address
280
+ # @param value [Boolean] true if public reading is allowed, false if it is not.
281
+ def set_public_read(value)
282
+ value = value ? true : false
283
+ self.update_attributes :public_read => value
284
+ #save
285
+ end
286
+
287
+ # Sets public write flag for this address
288
+ # @param value [Boolean] true if public writing is allowed, false if it is not.
289
+ def set_public_write(value)
290
+ value = value ? true : false
291
+ self.update_attributes :public_write => value
292
+ #save
293
+ end
294
+
295
+ # Sets permissions flag for this address, for a specific agent. The existing Chawk::Relationship will be destroyed and
296
+ # a new one created as specified. Write access is not yet checked.
297
+ # @param agent [Chawk::Agent] the agent to give permission.
298
+ # @param read [Boolean] true/false can the agent read this address.
299
+ # @param write [Boolean] true/false can the agent write this address. (Read acces is required to write.)
300
+ # @param admin [Boolean] does the agent have ownership/adnim rights for this address. (Read and write are granted if admin is as well.)
301
+ def set_permissions(agent,read=false,write=false,admin=false)
302
+ rels = relations.where(:agent_id => agent.id)
303
+ rels.delete_all()
304
+ rels = relations.where(:agent_id => agent.id)
305
+ if read || write || admin
306
+ vals = {agent:agent,read:(read ? true : false),write:(write ? true : false),admin:(admin ? true : false)}
307
+ relations.create(vals)
308
+ end
309
+ nil
310
+ end
311
+ end
312
+ end
313
+ end
data/lib/range.rb ADDED
@@ -0,0 +1,70 @@
1
+ require 'active_record'
2
+ module Chawk
3
+ module Models
4
+ class Range < ActiveRecord::Base
5
+ self.table_name_prefix = "chawk_"
6
+ validates :start_ts, :stop_ts, :beats, :parent_node, presence:true
7
+ validates :subkey, :data_node, absence: true
8
+ validate :order_is_correct
9
+
10
+ before_create :build_subnode
11
+
12
+ after_create :build_dataset
13
+ after_find :grant_node_access
14
+
15
+ belongs_to :parent_node, class_name:"Chawk::Models::Node"
16
+ belongs_to :data_node, class_name:"Chawk::Models::Node"
17
+
18
+ def reload
19
+ super
20
+ grant_node_access
21
+ end
22
+
23
+ def grant_node_access
24
+ # TODO: vet this very carefully.
25
+ # The only way to get here should be through an authorized source.
26
+ self.data_node.access=:full
27
+ end
28
+
29
+ def build_subnode
30
+ if subkey.to_s == ''
31
+ self.subkey = parent_node.key + "/" + SecureRandom.hex.to_s
32
+ end
33
+ self.data_node = Chawk::Models::Node.create(key:subkey)
34
+ grant_node_access
35
+ end
36
+
37
+ def order_is_correct
38
+ if self.start_ts >= self.stop_ts
39
+ errors.add(:stop_ts, "must be after start_ts.")
40
+ end
41
+ end
42
+
43
+ def build_dataset
44
+ populate!
45
+ end
46
+
47
+ def point_from_parent_point(now)
48
+ point = parent_node.points.where("observed_at <= :dt_to",{dt_to:now}).order(observed_at: :desc, id: :desc).first
49
+ if point
50
+ value = point.value
51
+ else
52
+ value = default || 0
53
+ end
54
+ data_node.points.create(observed_at:now, recorded_at:Time.now, value:value)
55
+ end
56
+
57
+ def populate!
58
+ # TODO: Accounting hook
59
+ # TODO: perform in callback (celluloid?)
60
+ self.data_node.points.destroy_all
61
+ step = 0.25 * self.beats
62
+ now = (self.start_ts*4).round/4.to_f
63
+ while now < self.stop_ts
64
+ point = point_from_parent_point now
65
+ now += step
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -5,58 +5,88 @@ describe Chawk do
5
5
  before do
6
6
  Chawk.clear_all_data!
7
7
  @agent = Chawk::Models::Agent.first || Chawk::Models::Agent.create(:name=>"Test User")
8
- @addr = Chawk.addr(@agent,'a:b')
8
+ @node = Chawk.node(@agent,'a:b')
9
9
  end
10
10
 
11
11
  it "has a good agent" do
12
- lambda {@addr = Chawk.addr(nil,'a:b')}.must_raise(ArgumentError)
13
- lambda {@addr = Chawk.addr(Object.new,'a:b')}.must_raise(ArgumentError)
14
- lambda {@addr = Chawk.addr(Chawk::Models::Agent,'a:b')}.must_raise(ArgumentError)
12
+ lambda {Chawk.node(nil,'a:b')}.must_raise(ArgumentError)
13
+ lambda {Chawk.node(Object.new,'a:b')}.must_raise(ArgumentError)
14
+ lambda {Chawk.node(Chawk::Models::Agent,'a:b')}.must_raise(ArgumentError)
15
+ node = Chawk.node(@agent,'a:b').key.must_equal("a:b")
16
+ agent2 = Chawk::Models::Agent.first || Chawk::Models::Agent.create(:name=>"Test User")
17
+ node = Chawk.node(agent2,'a:b').key.must_equal("a:b")
18
+ agent3 = Chawk::Models::Agent.create(:name=>"Test Failer")
19
+ lambda{Chawk.node(agent3,'a:b').key.must_equal("a:b")}.must_raise(SecurityError)
15
20
  end
16
21
 
17
22
  it "has key" do
18
- @addr.must_respond_to(:key)
23
+ @node.must_respond_to(:key)
19
24
  end
20
25
 
21
26
  it "key is valid" do
22
- @addr.key.must_equal("a:b")
23
- Chawk.addr(@agent,'a:b').key.must_equal("a:b")
24
- Chawk.addr(@agent,'0:x:z').key.must_equal("0:x:z")
27
+ @node.key.must_equal("a:b")
28
+ Chawk.node(@agent,'a:b').key.must_equal("a:b")
29
+ Chawk.node(@agent,'0:x:z').key.must_equal("0:x:z")
25
30
  path = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz012345689_:$!@*[]~()"
26
- Chawk.addr(@agent,path).key.must_equal(path)
31
+ Chawk.node(@agent,path).key.must_equal(path)
27
32
  end
28
33
 
29
34
  it "rejects invalid paths" do
30
- lambda {Chawk.addr(@agent,['A'])}.must_raise(ArgumentError)
31
- lambda {Chawk.addr(@agent,0)}.must_raise(ArgumentError)
32
- lambda {Chawk.addr(@agent,:a)}.must_raise(ArgumentError)
33
- lambda {Chawk.addr(@agent,Object.new)}.must_raise(ArgumentError)
34
- lambda {Chawk.addr(@agent,String.new)}.must_raise(ArgumentError)
35
- lambda {Chawk.addr(@agent,"")}.must_raise(ArgumentError)
36
- lambda {Chawk.addr(@agent,"/")}.must_raise(ArgumentError)
37
- lambda {Chawk.addr(@agent,"\\")}.must_raise(ArgumentError)
38
- lambda {Chawk.addr(@agent,"&")}.must_raise(ArgumentError)
39
- lambda {Chawk.addr(@agent,"?")}.must_raise(ArgumentError)
35
+ lambda {Chawk.node(@agent,['A'])}.must_raise(ArgumentError)
36
+ lambda {Chawk.node(@agent,0)}.must_raise(ArgumentError)
37
+ lambda {Chawk.node(@agent,:a)}.must_raise(ArgumentError)
38
+ lambda {Chawk.node(@agent,Object.new)}.must_raise(ArgumentError)
39
+ lambda {Chawk.node(@agent,String.new)}.must_raise(ArgumentError)
40
+ lambda {Chawk.node(@agent,"")}.must_raise(ArgumentError)
41
+ lambda {Chawk.node(@agent,"/")}.must_raise(ArgumentError)
42
+ lambda {Chawk.node(@agent,"\\")}.must_raise(ArgumentError)
43
+ lambda {Chawk.node(@agent,"&")}.must_raise(ArgumentError)
44
+ lambda {Chawk.node(@agent,"?")}.must_raise(ArgumentError)
40
45
  end
41
46
 
42
47
  it "sets permissions" do
43
- @addr.must_respond_to(:set_public_read)
44
- @addr.must_respond_to(:set_permissions)
48
+ @node.must_respond_to(:set_public_read)
49
+ @node.must_respond_to(:set_permissions)
45
50
  end
46
51
 
47
52
  it "stops unauthorized access" do
48
- agent2 = Chawk::Models::Agent.where("name=?","Steve Austin").first || Chawk::Models::Agent.create(name:"Steve Austin")
49
- lambda{@addr = Chawk.addr(agent2,'a:b')}.must_raise SecurityError
50
- @addr.set_public_read true
51
- Chawk.addr(agent2,'a:b').key.must_equal "a:b"
52
- @addr.set_public_read false
53
- lambda{@addr = Chawk.addr(agent2,'a:b')}.must_raise SecurityError
54
- @addr.set_permissions(agent2,true,false,false)
55
- Chawk.addr(agent2,'a:b').key.must_equal "a:b"
56
- @addr.set_permissions(agent2,false,false,false)
57
- lambda{@addr = Chawk.addr(agent2,'a:b')}.must_raise SecurityError
58
- @addr.set_permissions(agent2,false,false,true)
59
- Chawk.addr(agent2,'a:b').key.must_equal "a:b"
53
+ @node.set_permissions(@agent,true,false,false)
54
+ @node = Chawk::Models::Node.find(@node.id)
55
+ lambda{Chawk.node(@agent,'a:b')}.must_raise SecurityError
56
+ @node.set_permissions(@agent,false,false,false)
57
+ lambda{Chawk.node(@agent,'a:b')}.must_raise SecurityError
58
+ end
59
+
60
+ it "stops unauthorized reads" do
61
+ @node.set_permissions(@agent,false,false,false)
62
+ lambda{Chawk.node(@agent,'a:b', :write)}.must_raise SecurityError
63
+ @node.set_public_read true
64
+ Chawk.node(@agent,'a:b', :read).key.must_equal "a:b"
65
+ @node.set_public_read false
66
+ lambda{Chawk.node(@agent,'a:b', :read)}.must_raise SecurityError
67
+ @node.set_permissions(@agent,true,true,false)
68
+ w_node = Chawk.node(@agent,'a:b', :write)
69
+ lambda{w_node.values_range(0,0)}.must_raise SecurityError
70
+ end
71
+
72
+ it "stops unauthorized writes" do
73
+ @node.set_permissions(@agent,true,false,false)
74
+ lambda{Chawk.node(@agent,'a:b', :write)}.must_raise SecurityError
75
+ @node.set_public_write true
76
+ Chawk.node(@agent,'a:b', :write).key.must_equal "a:b"
77
+ @node.set_public_write false
78
+ lambda{Chawk.node(@agent,'a:b', :write)}.must_raise SecurityError
79
+ r_node = Chawk.node(@agent,'a:b', :read)
80
+ lambda{r_node.add_points([1,2,3,4])}.must_raise SecurityError
81
+ end
82
+
83
+ it "stops unauthorized admin" do
84
+ @node.set_permissions(@agent,false,false,false)
85
+ lambda{Chawk.node(@agent,'a:b', :admin)}.must_raise SecurityError
86
+ @node.set_permissions(@agent,false,true,true)
87
+ Chawk.node(@agent,'a:b', :admin).key.must_equal "a:b"
88
+ w_node = Chawk.node(@agent,'a:b', :write)
89
+ lambda{w_node.clear_points!}.must_raise SecurityError
60
90
  end
61
91
 
62
92
  end
@@ -2,7 +2,7 @@ require 'test_helper'
2
2
  require 'json'
3
3
 
4
4
  Jiggy = Chawk.clone
5
- def Jiggy.find_or_create_node(agent,key) return nil end
5
+ def Jiggy.find_or_create_node(agent,key,access=:full) return nil end
6
6
 
7
7
  describe Jiggy do
8
8
  before do
@@ -12,7 +12,7 @@ describe Jiggy do
12
12
 
13
13
  it "needs a valid node" do
14
14
 
15
- lambda {@addr = Jiggy.addr(@agent,'a:b')}.must_raise(ArgumentError)
15
+ lambda {@node = Jiggy.node(@agent,'a:b')}.must_raise(ArgumentError)
16
16
  end
17
17
 
18
18
  end