ag 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d52f18a80b6c4e34a312724af075fb150886ff5d
4
+ data.tar.gz: 3f27cf30c2fc9fa720f9d9b34102b5beed1715cc
5
+ SHA512:
6
+ metadata.gz: b3cd6bdc3b40ed3615ebb1061db3179af0b2168dca2a653644d067b8fd206f237f92c7371c8b8ac1dabf4adccb4ec43ca4a5d46b1b1015e38bbb691d16c1703f
7
+ data.tar.gz: 28c51284f992d75aaef330826abd918b759ddb3fc128f217fab327496c48b9136ef613b4861ec816fed2f64d0f6127cd99511e0c0d41875c112656693552ff32
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+ gemspec
3
+
4
+ gem "sequel", "~> 4.19.0"
5
+ gem "sqlite3", "~> 1.3.10"
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 John Nunemaker
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,64 @@
1
+ # Ag
2
+
3
+ WORK IN PROGRESS...
4
+
5
+ Experiments in describing feeds/timelines of events in code based on adapters so things can work at most levels of throughput.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem "ag"
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install ag
22
+
23
+ ## Usage
24
+
25
+ ```ruby
26
+ require "ag"
27
+
28
+ adapter = Ag::Adapters::Memory.new
29
+ client = Ag::Client.new(adapter)
30
+ john = Ag::Object.new("User", "1")
31
+ steve = Ag::Object.new("User", "2")
32
+ presentation = Ag::Object.new("Presentation", "1")
33
+ event = Ag::Event.new({
34
+ producer: steve,
35
+ object: presentation,
36
+ verb: "upload_presentation",
37
+ })
38
+
39
+ # connect john to steve
40
+ pp connect: client.connect(john, steve)
41
+
42
+ # is john connected to steve
43
+ pp connected?: client.connected?(john, steve)
44
+
45
+ # consumers of steve
46
+ pp consumers: client.consumers(steve)
47
+
48
+ # producers john is connected to
49
+ pp producers: client.producers(john)
50
+
51
+ # produce an event for steve
52
+ pp produce: client.produce(event)
53
+
54
+ # get the timeline of events for john based on the producers john follows
55
+ pp timeline: client.timeline(john)
56
+ ```
57
+
58
+ ## Contributing
59
+
60
+ 1. Fork it ( https://github.com/jnunemaker/ag/fork )
61
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
62
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
63
+ 4. Push to the branch (`git push origin my-new-feature`)
64
+ 5. Create a new Pull Request
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "ag/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "ag"
8
+ spec.version = Ag::VERSION
9
+ spec.authors = ["John Nunemaker"]
10
+ spec.email = ["nunemaker@gmail.com"]
11
+ spec.summary = %q{Producers, consumers, connections and timelines.}
12
+ spec.description = %q{Producers, consumers, connections and timelines.}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.7"
22
+ spec.add_development_dependency "rake", "~> 10.0"
23
+ spec.add_development_dependency "minitest", "~> 5.5.1"
24
+ end
@@ -0,0 +1,31 @@
1
+ require_relative "setup"
2
+ require "ag"
3
+
4
+ adapter = Ag::Adapters::Memory.new
5
+ client = Ag::Client.new(adapter)
6
+ john = Ag::Object.new("User", "1")
7
+ steve = Ag::Object.new("User", "2")
8
+ presentation = Ag::Object.new("Presentation", "1")
9
+ event = Ag::Event.new({
10
+ producer: steve,
11
+ object: presentation,
12
+ verb: "upload_presentation",
13
+ })
14
+
15
+ # connect john to steve
16
+ pp connect: client.connect(john, steve)
17
+
18
+ # is john connected to steve
19
+ pp connected?: client.connected?(john, steve)
20
+
21
+ # consumers of steve
22
+ pp consumers: client.consumers(steve)
23
+
24
+ # producers john is connected to
25
+ pp producers: client.producers(john)
26
+
27
+ # produce an event for steve
28
+ pp produce: client.produce(event)
29
+
30
+ # get the timeline of events for john based on the producers john follows
31
+ pp timeline: client.timeline(john)
@@ -0,0 +1,8 @@
1
+ # Nothing to see here... move along.
2
+ # Sets up load path for examples and requires some stuff
3
+ require "pp"
4
+ require "pathname"
5
+
6
+ root_path = Pathname(__FILE__).dirname.join("..").expand_path
7
+ lib_path = root_path.join("lib")
8
+ $:.unshift(lib_path)
@@ -0,0 +1,11 @@
1
+ require "ag/version"
2
+
3
+ module Ag
4
+ end
5
+
6
+ require "ag/object"
7
+ require "ag/connection"
8
+ require "ag/event"
9
+ require "ag/feed"
10
+ require "ag/client"
11
+ require "ag/adapters/memory"
@@ -0,0 +1,62 @@
1
+ require "securerandom"
2
+
3
+ module Ag
4
+ module Adapters
5
+ class Memory
6
+ def initialize(source = {})
7
+ @source = source
8
+ @source[:connections] ||= []
9
+ @source[:events] ||= []
10
+ end
11
+
12
+ def connect(consumer, producer)
13
+ @source[:connections] << Connection.new({
14
+ id: SecureRandom.uuid,
15
+ created_at: Time.now.utc,
16
+ consumer: consumer,
17
+ producer: producer,
18
+ })
19
+ end
20
+
21
+ def produce(event)
22
+ result = Ag::Event.new({
23
+ id: SecureRandom.uuid,
24
+ producer: event.producer,
25
+ object: event.object,
26
+ verb: event.verb,
27
+ })
28
+ @source[:events] << result
29
+ result
30
+ end
31
+
32
+ def connected?(consumer, producer)
33
+ !@source[:connections].detect { |connection|
34
+ connection.consumer == consumer &&
35
+ connection.producer == producer
36
+ }.nil?
37
+ end
38
+
39
+ def consumers(producer, options = {})
40
+ @source[:connections].select { |connection|
41
+ connection.producer == producer
42
+ }.reverse[options.fetch(:offset, 0), options.fetch(:limit, 30)]
43
+ end
44
+
45
+ def producers(consumer, options = {})
46
+ @source[:connections].select { |connection|
47
+ connection.consumer == consumer
48
+ }.reverse[options.fetch(:offset, 0), options.fetch(:limit, 30)]
49
+ end
50
+
51
+ def timeline(consumer, options = {})
52
+ producers = producers(consumer).map(&:producer)
53
+
54
+ Array(@source[:events]).select { |event|
55
+ producers.include?(event.producer)
56
+ }.sort_by { |event|
57
+ -event.created_at.to_f
58
+ }[options.fetch(:offset, 0), options.fetch(:limit, 30)]
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,174 @@
1
+ require "sequel"
2
+
3
+ module Ag
4
+ module Adapters
5
+ # Adapter that uses the minimum amount of writes while still allowing full
6
+ # historic assembly of timelines. This comes at the cost of slower reads.
7
+ class SequelPull
8
+ def initialize(db)
9
+ @db = db
10
+ end
11
+
12
+ def connect(consumer, producer)
13
+ created_at = Time.now.utc
14
+ id = @db[:connections].insert({
15
+ consumer_id: consumer.id,
16
+ consumer_type: consumer.type,
17
+ producer_id: producer.id,
18
+ producer_type: producer.type,
19
+ created_at: created_at,
20
+ })
21
+
22
+ Connection.new({
23
+ id: id,
24
+ created_at: created_at,
25
+ consumer: consumer,
26
+ producer: producer,
27
+ })
28
+ end
29
+
30
+ def produce(event)
31
+ created_at = Time.now.utc
32
+ id = @db[:events].insert({
33
+ producer_type: event.producer.type,
34
+ producer_id: event.producer.id,
35
+ object_type: event.object.type,
36
+ object_id: event.object.id,
37
+ verb: event.verb,
38
+ created_at: created_at,
39
+ })
40
+
41
+ Ag::Event.new({
42
+ id: id,
43
+ created_at: created_at,
44
+ producer: event.producer,
45
+ object: event.object,
46
+ verb: event.verb,
47
+ })
48
+ end
49
+
50
+ def connected?(consumer, producer)
51
+ statement = <<-SQL
52
+ SELECT
53
+ 1
54
+ FROM
55
+ connections
56
+ WHERE
57
+ consumer_id = :consumer_id AND
58
+ producer_id = :producer_id
59
+ LIMIT 1
60
+ SQL
61
+
62
+ binds = {
63
+ consumer_id: consumer.id,
64
+ consumer_type: consumer.type,
65
+ producer_id: producer.id,
66
+ producer_type: producer.type,
67
+ }
68
+
69
+ !@db[statement, binds].first.nil?
70
+ end
71
+
72
+ def consumers(producer, options = {})
73
+ statement = <<-SQL
74
+ SELECT
75
+ id, created_at, consumer_id, consumer_type, producer_id, producer_type
76
+ FROM
77
+ connections
78
+ WHERE
79
+ producer_id = :producer_id AND
80
+ producer_type = :producer_type
81
+ ORDER BY
82
+ id DESC
83
+ LIMIT :limit
84
+ OFFSET :offset
85
+ SQL
86
+
87
+ binds = {
88
+ producer_id: producer.id,
89
+ producer_type: producer.type,
90
+ limit: options.fetch(:limit, 30),
91
+ offset: options.fetch(:offset, 0),
92
+ }
93
+
94
+ @db[statement, binds].to_a.map { |row|
95
+ Connection.new({
96
+ id: row[:id],
97
+ created_at: row[:created_at],
98
+ consumer: Object.new(row[:consumer_type], row[:consumer_id]),
99
+ producer: Object.new(row[:producer_type], row[:producer_id]),
100
+ })
101
+ }
102
+ end
103
+
104
+ def producers(consumer, options = {})
105
+ statement = <<-SQL
106
+ SELECT
107
+ id, created_at, consumer_id, consumer_type, producer_id, producer_type
108
+ FROM
109
+ connections
110
+ WHERE
111
+ consumer_id = :consumer_id AND
112
+ consumer_type = :consumer_type
113
+ ORDER BY
114
+ id DESC
115
+ LIMIT :limit
116
+ OFFSET :offset
117
+ SQL
118
+
119
+ binds = {
120
+ consumer_id: consumer.id,
121
+ consumer_type: consumer.type,
122
+ limit: options.fetch(:limit, 30),
123
+ offset: options.fetch(:offset, 0),
124
+ }
125
+
126
+ @db[statement, binds].to_a.map { |row|
127
+ Connection.new({
128
+ id: row[:id],
129
+ created_at: row[:created_at],
130
+ consumer: Object.new(row[:consumer_type], row[:consumer_id]),
131
+ producer: Object.new(row[:producer_type], row[:producer_id]),
132
+ })
133
+ }
134
+ end
135
+
136
+ def timeline(consumer, options = {})
137
+ statement = <<-SQL
138
+ SELECT
139
+ e.*
140
+ FROM
141
+ events e
142
+ INNER JOIN
143
+ connections c ON
144
+ e.producer_id = c.producer_id AND
145
+ e.producer_type = c.producer_type
146
+ WHERE
147
+ c.consumer_id = :consumer_id AND
148
+ c.consumer_type = :consumer_type
149
+ ORDER BY
150
+ e.created_at DESC
151
+ LIMIT :limit
152
+ OFFSET :offset
153
+ SQL
154
+
155
+ binds = {
156
+ consumer_id: consumer.id,
157
+ consumer_type: consumer.type,
158
+ limit: options.fetch(:limit, 30),
159
+ offset: options.fetch(:offset, 0),
160
+ }
161
+
162
+ @db[statement, binds].to_a.map { |row|
163
+ Ag::Event.new({
164
+ id: row[:id],
165
+ created_at: row[:created_at],
166
+ producer: Ag::Object.new(row[:producer_type], row[:producer_id]),
167
+ object: Ag::Object.new(row[:object_type], row[:object_id]),
168
+ verb: row[:verb],
169
+ })
170
+ }
171
+ end
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,169 @@
1
+ require "sequel"
2
+
3
+ module Ag
4
+ module Adapters
5
+ # Adapter that uses the minimum amount of writes while still allowing full
6
+ # historic assembly of timelines. This comes at the cost of slower reads.
7
+ class SequelPullCompact
8
+ def self.dehydrate(object)
9
+ [object.type, object.id].join(":")
10
+ end
11
+
12
+ def self.hydrate(id)
13
+ Ag::Object.new(*id.split(":"))
14
+ end
15
+
16
+ def initialize(db)
17
+ @db = db
18
+ end
19
+
20
+ def connect(consumer, producer)
21
+ created_at = Time.now.utc
22
+ id = @db[:connections].insert({
23
+ consumer_id: self.class.dehydrate(consumer),
24
+ producer_id: self.class.dehydrate(producer),
25
+ created_at: created_at,
26
+ })
27
+
28
+ Connection.new({
29
+ id: id,
30
+ created_at: created_at,
31
+ consumer: consumer,
32
+ producer: producer,
33
+ })
34
+ end
35
+
36
+ def produce(event)
37
+ created_at = event.created_at || Time.now.utc
38
+ id = @db[:events].insert({
39
+ producer_id: self.class.dehydrate(event.producer),
40
+ object_id: self.class.dehydrate(event.object),
41
+ verb: event.verb,
42
+ created_at: created_at,
43
+ })
44
+
45
+ Ag::Event.new({
46
+ id: id,
47
+ created_at: created_at,
48
+ producer: event.producer,
49
+ object: event.object,
50
+ verb: event.verb,
51
+ })
52
+ end
53
+
54
+ def connected?(consumer, producer)
55
+ statement = <<-SQL
56
+ SELECT
57
+ 1
58
+ FROM
59
+ connections
60
+ WHERE
61
+ consumer_id = :consumer_id AND
62
+ producer_id = :producer_id
63
+ LIMIT 1
64
+ SQL
65
+
66
+ binds = {
67
+ consumer_id: self.class.dehydrate(consumer),
68
+ producer_id: self.class.dehydrate(producer),
69
+ }
70
+
71
+ !@db[statement, binds].first.nil?
72
+ end
73
+
74
+ def consumers(producer, options = {})
75
+ statement = <<-SQL
76
+ SELECT
77
+ id, created_at, consumer_id, producer_id
78
+ FROM
79
+ connections
80
+ WHERE
81
+ producer_id = :producer_id
82
+ ORDER BY
83
+ id DESC
84
+ LIMIT :limit
85
+ OFFSET :offset
86
+ SQL
87
+
88
+ binds = {
89
+ producer_id: self.class.dehydrate(producer),
90
+ limit: options.fetch(:limit, 30),
91
+ offset: options.fetch(:offset, 0),
92
+ }
93
+
94
+ @db[statement, binds].to_a.map { |row|
95
+ Connection.new({
96
+ id: row[:id],
97
+ created_at: row[:created_at],
98
+ consumer: self.class.hydrate(row[:consumer_id]),
99
+ producer: self.class.hydrate(row[:producer_id]),
100
+ })
101
+ }
102
+ end
103
+
104
+ def producers(consumer, options = {})
105
+ statement = <<-SQL
106
+ SELECT
107
+ id, created_at, consumer_id, producer_id
108
+ FROM
109
+ connections
110
+ WHERE
111
+ consumer_id = :consumer_id
112
+ ORDER BY
113
+ id DESC
114
+ LIMIT :limit
115
+ OFFSET :offset
116
+ SQL
117
+
118
+ binds = {
119
+ consumer_id: self.class.dehydrate(consumer),
120
+ limit: options.fetch(:limit, 30),
121
+ offset: options.fetch(:offset, 0),
122
+ }
123
+
124
+ @db[statement, binds].to_a.map { |row|
125
+ Connection.new({
126
+ id: row[:id],
127
+ created_at: row[:created_at],
128
+ consumer: self.class.hydrate(row[:consumer_id]),
129
+ producer: self.class.hydrate(row[:producer_id]),
130
+ })
131
+ }
132
+ end
133
+
134
+ def timeline(consumer, options = {})
135
+ statement = <<-SQL
136
+ SELECT
137
+ e.*
138
+ FROM
139
+ events e
140
+ INNER JOIN
141
+ connections c ON
142
+ e.producer_id = c.producer_id
143
+ WHERE
144
+ c.consumer_id = :consumer_id
145
+ ORDER BY
146
+ e.created_at DESC
147
+ LIMIT :limit
148
+ OFFSET :offset
149
+ SQL
150
+
151
+ binds = {
152
+ consumer_id: self.class.dehydrate(consumer),
153
+ limit: options.fetch(:limit, 30),
154
+ offset: options.fetch(:offset, 0),
155
+ }
156
+
157
+ @db[statement, binds].to_a.map { |row|
158
+ Ag::Event.new({
159
+ id: row[:id],
160
+ created_at: row[:created_at],
161
+ producer: self.class.hydrate(row[:producer_id]),
162
+ object: self.class.hydrate(row[:object_id]),
163
+ verb: row[:verb],
164
+ })
165
+ }
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,31 @@
1
+ module Ag
2
+ class Client
3
+ def initialize(adapter)
4
+ @adapter = adapter
5
+ end
6
+
7
+ def connect(consumer, producer)
8
+ @adapter.connect(consumer, producer)
9
+ end
10
+
11
+ def produce(event)
12
+ @adapter.produce(event)
13
+ end
14
+
15
+ def connected?(consumer, producer)
16
+ @adapter.connected?(consumer, producer)
17
+ end
18
+
19
+ def consumers(producer, options = {})
20
+ @adapter.consumers(producer, options)
21
+ end
22
+
23
+ def producers(consumer, options = {})
24
+ @adapter.producers(consumer, options)
25
+ end
26
+
27
+ def timeline(consumer, options = {})
28
+ @adapter.timeline(consumer, options)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,17 @@
1
+ require "forwardable"
2
+
3
+ module Ag
4
+ class Connection
5
+ attr_reader :id
6
+ attr_reader :producer
7
+ attr_reader :consumer
8
+ attr_reader :created_at
9
+
10
+ def initialize(attributes = {})
11
+ @id = attributes[:id]
12
+ @producer = attributes[:producer]
13
+ @consumer = attributes[:consumer]
14
+ @created_at = attributes[:created_at]
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ module Ag
2
+ class Event
3
+ attr_reader :id
4
+ attr_reader :producer
5
+ attr_reader :object
6
+ attr_reader :verb
7
+ attr_reader :created_at
8
+
9
+ def initialize(attrs = {})
10
+ @id = attrs[:id]
11
+ @producer = attrs.fetch(:producer)
12
+ @object = attrs.fetch(:object)
13
+ @verb = attrs.fetch(:verb)
14
+ @created_at = attrs.fetch(:created_at) { Time.now.utc }
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,4 @@
1
+ module Ag
2
+ class Feed
3
+ end
4
+ end
@@ -0,0 +1,17 @@
1
+ module Ag
2
+ class Object
3
+ attr_reader :type
4
+ attr_reader :id
5
+
6
+ def initialize(type, id)
7
+ @type = type
8
+ @id = id
9
+ end
10
+
11
+ def ==(other)
12
+ self.class == other.class &&
13
+ self.type == other.type &&
14
+ self.id == other.id
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,179 @@
1
+ module Ag
2
+ module Spec
3
+ module Adapter
4
+ def test_connect
5
+ consumer = Ag::Object.new("User", "1")
6
+ producer = Ag::Object.new("User", "2")
7
+
8
+ adapter.connect(consumer, producer)
9
+
10
+ connection = connections.first
11
+ assert_equal consumer.id, connection.consumer.id
12
+ assert_equal consumer.type, connection.consumer.type
13
+ assert_equal producer.id, connection.producer.id
14
+ assert_equal producer.type, connection.producer.type
15
+ assert_in_delta Time.now.utc, connection.created_at, 1
16
+ end
17
+
18
+ def test_produce
19
+ producer = Ag::Object.new("User", "1")
20
+ object = Ag::Object.new("User", "2")
21
+ event = Ag::Event.new({
22
+ producer: producer,
23
+ object: object,
24
+ verb: "follow",
25
+ })
26
+
27
+ result = adapter.produce(event)
28
+
29
+ event = events.first
30
+ assert_equal event.id, result.id
31
+ assert_equal producer.id, event.producer.id
32
+ assert_equal producer.type, event.producer.type
33
+ assert_equal object.id, event.object.id
34
+ assert_equal object.type, event.object.type
35
+ assert_in_delta Time.now.utc, event.created_at, 1
36
+ end
37
+
38
+ def test_connected
39
+ consumer = Ag::Object.new("User", "1")
40
+ producer = Ag::Object.new("User", "2")
41
+ connect(consumer, producer)
42
+
43
+ assert_equal true, adapter.connected?(consumer, producer)
44
+ assert_equal false, adapter.connected?(producer, consumer)
45
+ end
46
+
47
+ def test_consumers
48
+ consumer1 = Ag::Object.new("User", "1")
49
+ consumer2 = Ag::Object.new("User", "2")
50
+ consumer3 = Ag::Object.new("User", "3")
51
+ producer = Ag::Object.new("User", "4")
52
+ connect(consumer1, producer)
53
+ connect(consumer2, producer)
54
+
55
+ consumers = adapter.consumers(producer)
56
+ assert_equal 2, consumers.size
57
+ assert_equal "2", consumers[0].consumer.id
58
+ assert_equal "1", consumers[1].consumer.id
59
+ end
60
+
61
+ def test_consumers_limit
62
+ producer = Ag::Object.new("User", "99")
63
+ consumers = (1..10).to_a.map { |n|
64
+ Ag::Object.new("User", n.to_s).tap { |consumer|
65
+ connect consumer, producer
66
+ }
67
+ }
68
+ assert_equal 5, adapter.consumers(producer, limit: 5).size
69
+ assert_equal consumers[5..9].reverse,
70
+ adapter.consumers(producer, limit: 5).map(&:consumer)
71
+ end
72
+
73
+ def test_consumers_offset
74
+ producer = Ag::Object.new("User", "99")
75
+ consumers = (1..10).to_a.map { |n|
76
+ Ag::Object.new("User", n.to_s).tap { |consumer|
77
+ connect consumer, producer
78
+ }
79
+ }
80
+ assert_equal consumers[0..4].reverse,
81
+ adapter.consumers(producer, offset: 5).map(&:consumer)
82
+ end
83
+
84
+ def test_producers
85
+ consumer1 = Ag::Object.new("User", "1")
86
+ consumer2 = Ag::Object.new("User", "2")
87
+ producer1 = Ag::Object.new("User", "3")
88
+ producer2 = Ag::Object.new("User", "4")
89
+ producer3 = Ag::Object.new("User", "5")
90
+ connect(consumer1, producer1)
91
+ connect(consumer1, producer2)
92
+ connect(consumer2, producer3)
93
+
94
+ producers = adapter.producers(consumer1)
95
+ assert_equal 2, producers.size
96
+ assert_equal "4", producers[0].producer.id
97
+ assert_equal "3", producers[1].producer.id
98
+ end
99
+
100
+ def test_producers_limit
101
+ consumer = Ag::Object.new("User", "99")
102
+ producers = (1..10).to_a.map { |n|
103
+ Ag::Object.new("User", n.to_s).tap { |producer|
104
+ connect consumer, producer
105
+ }
106
+ }
107
+ assert_equal 5, adapter.producers(consumer, limit: 5).size
108
+ assert_equal producers[5..9].reverse,
109
+ adapter.producers(consumer, limit: 5).map(&:producer)
110
+ end
111
+
112
+ def test_producers_offset
113
+ consumer = Ag::Object.new("User", "99")
114
+ producers = (1..10).to_a.map { |n|
115
+ Ag::Object.new("User", n.to_s).tap { |producer|
116
+ connect consumer, producer
117
+ }
118
+ }
119
+ assert_equal producers[0..4].reverse,
120
+ adapter.producers(consumer, offset: 5).map(&:producer)
121
+ end
122
+
123
+ def test_timeline
124
+ john = Ag::Object.new("User", "1")
125
+ steve = Ag::Object.new("User", "2")
126
+ presentation = Ag::Object.new("Presentation", "1")
127
+ connect john, steve
128
+ produce Ag::Event.new(producer: steve, object: presentation, verb: "publish")
129
+
130
+ events = adapter.timeline(john)
131
+ assert_equal 1, events.size
132
+ end
133
+
134
+ def test_timeline_limit
135
+ john = Ag::Object.new("User", "1")
136
+ steve = Ag::Object.new("User", "2")
137
+ connect john, steve
138
+
139
+ presentations = (1..10).to_a.map { |n|
140
+ Ag::Object.new("Presentation", n.to_s)
141
+ }
142
+
143
+ presentations.each do |presentation|
144
+ produce Ag::Event.new({
145
+ producer: steve,
146
+ object: presentation,
147
+ verb: "publish",
148
+ })
149
+ end
150
+
151
+ events = adapter.timeline(john, limit: 5)
152
+ assert_equal 5, events.size
153
+ assert_equal presentations[5..9].reverse, events.map(&:object)
154
+ end
155
+
156
+ def test_timeline_offset
157
+ john = Ag::Object.new("User", "1")
158
+ steve = Ag::Object.new("User", "2")
159
+ connect john, steve
160
+
161
+ presentations = (1..10).to_a.map { |n|
162
+ Ag::Object.new("Presentation", n.to_s)
163
+ }
164
+
165
+ presentations.each do |presentation|
166
+ produce Ag::Event.new({
167
+ producer: steve,
168
+ object: presentation,
169
+ verb: "publish",
170
+ })
171
+ end
172
+
173
+ events = adapter.timeline(john, offset: 5)
174
+ assert_equal 5, events.size
175
+ assert_equal presentations[0..4].reverse, events.map(&:object)
176
+ end
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,3 @@
1
+ module Ag
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1 @@
1
+ bundle --quiet
@@ -0,0 +1,25 @@
1
+ #!/bin/sh
2
+ #/ Usage: test [individual test file]
3
+ #/
4
+ #/ Bootstrap and run all tests or an individual test.
5
+ #/
6
+ #/ Examples:
7
+ #/
8
+ #/ # run all tests
9
+ #/ test
10
+ #/
11
+ #/ # run individual test
12
+ #/ test test/controller_instrumentation_test.rb
13
+ #/
14
+
15
+ set -e
16
+ cd $(dirname "$0")/..
17
+
18
+ [ "$1" = "--help" -o "$1" = "-h" -o "$1" = "help" ] && {
19
+ grep '^#/' <"$0"| cut -c4-
20
+ exit 0
21
+ }
22
+
23
+ ruby -I lib -I test -r rubygems \
24
+ -e 'require "bundler/setup"' \
25
+ -e '(ARGV.empty? ? Dir["test/**/*_test.rb"] : ARGV).each { |f| load f }' -- "$@"
@@ -0,0 +1,41 @@
1
+ require_relative "../helper"
2
+ require "ag/adapters/memory"
3
+ require "ag/spec/adapter"
4
+ require "securerandom"
5
+
6
+ class AdaptersMemoryTest < Ag::Test
7
+ def setup
8
+ @source = {}
9
+ end
10
+
11
+ def adapter
12
+ @adapter ||= Ag::Adapters::Memory.new(@source)
13
+ end
14
+
15
+ include Ag::Spec::Adapter
16
+
17
+ private
18
+
19
+ def connections
20
+ @source[:connections]
21
+ end
22
+
23
+ def events
24
+ @source[:events]
25
+ end
26
+
27
+ def connect(consumer, producer)
28
+ @source[:connections] ||= []
29
+ @source[:connections] << Ag::Connection.new({
30
+ id: SecureRandom.uuid,
31
+ created_at: Time.now.utc,
32
+ consumer: consumer,
33
+ producer: producer,
34
+ })
35
+ end
36
+
37
+ def produce(event)
38
+ @source[:events] ||= []
39
+ @source[:events] << event
40
+ end
41
+ end
@@ -0,0 +1,72 @@
1
+ require_relative "../helper"
2
+ require "ag/adapters/sequel_pull_compact"
3
+ require "ag/spec/adapter"
4
+
5
+ class AdaptersSequelPullCompactTest < Ag::Test
6
+ def setup
7
+ Sequel.default_timezone = :utc
8
+ @db = Sequel.sqlite
9
+
10
+ @db.create_table :connections do
11
+ primary_key :id
12
+ String :consumer_id
13
+ String :producer_id
14
+ Time :created_at
15
+ index [:consumer_id, :producer_id], unique: true
16
+ end
17
+
18
+ @db.create_table :events do
19
+ primary_key :id
20
+ String :producer_id
21
+ String :object_id
22
+ String :verb
23
+ Time :created_at
24
+ end
25
+ end
26
+
27
+ def adapter
28
+ @adapter ||= Ag::Adapters::SequelPullCompact.new(@db)
29
+ end
30
+
31
+ include Ag::Spec::Adapter
32
+
33
+ private
34
+
35
+ def connections
36
+ @db[:connections].map { |row|
37
+ Ag::Connection.new({
38
+ id: row[:id],
39
+ created_at: row[:created_at],
40
+ consumer: Ag::Adapters::SequelPullCompact.hydrate(row[:consumer_id]),
41
+ producer: Ag::Adapters::SequelPullCompact.hydrate(row[:producer_id]),
42
+ })
43
+ }
44
+ end
45
+
46
+ def events
47
+ @db[:events].map { |row|
48
+ Ag::Event.new({
49
+ id: row[:id],
50
+ verb: row[:verb],
51
+ created_at: row[:created_at],
52
+ producer: Ag::Adapters::SequelPullCompact.hydrate(row[:producer_id]),
53
+ object: Ag::Adapters::SequelPullCompact.hydrate(row[:object_id]),
54
+ })
55
+ }
56
+ end
57
+
58
+ def connect(consumer, producer)
59
+ @db[:connections].insert({
60
+ consumer_id: Ag::Adapters::SequelPullCompact.dehydrate(consumer),
61
+ producer_id: Ag::Adapters::SequelPullCompact.dehydrate(producer),
62
+ })
63
+ end
64
+
65
+ def produce(event)
66
+ @db[:events].insert({
67
+ producer_id: Ag::Adapters::SequelPullCompact.dehydrate(event.producer),
68
+ object_id: Ag::Adapters::SequelPullCompact.dehydrate(event.object),
69
+ created_at: Time.now.utc,
70
+ })
71
+ end
72
+ end
@@ -0,0 +1,80 @@
1
+ require_relative "../helper"
2
+ require "ag/adapters/sequel_pull"
3
+ require "ag/spec/adapter"
4
+
5
+ class AdaptersSequelPullTest < Ag::Test
6
+ def setup
7
+ Sequel.default_timezone = :utc
8
+ @db = Sequel.sqlite
9
+
10
+ @db.create_table :connections do
11
+ primary_key :id
12
+ String :consumer_id
13
+ String :consumer_type
14
+ String :producer_id
15
+ String :producer_type
16
+ Time :created_at
17
+ index [:consumer_id, :consumer_type, :producer_id, :producer_type], unique: true
18
+ end
19
+
20
+ @db.create_table :events do
21
+ primary_key :id
22
+ String :producer_id
23
+ String :producer_type
24
+ String :object_id
25
+ String :object_type
26
+ String :verb
27
+ Time :created_at
28
+ end
29
+ end
30
+
31
+ def adapter
32
+ @adapter ||= Ag::Adapters::SequelPull.new(@db)
33
+ end
34
+
35
+ include Ag::Spec::Adapter
36
+
37
+ private
38
+
39
+ def connections
40
+ @db[:connections].map { |row|
41
+ Ag::Connection.new({
42
+ id: row[:id],
43
+ created_at: row[:created_at],
44
+ consumer: Ag::Object.new(row[:consumer_type], row[:consumer_id]),
45
+ producer: Ag::Object.new(row[:producer_type], row[:producer_id]),
46
+ })
47
+ }
48
+ end
49
+
50
+ def events
51
+ @db[:events].map { |row|
52
+ Ag::Event.new({
53
+ id: row[:id],
54
+ verb: row[:verb],
55
+ created_at: row[:created_at],
56
+ producer: Ag::Object.new(row[:producer_type], row[:producer_id]),
57
+ object: Ag::Object.new(row[:object_type], row[:object_id]),
58
+ })
59
+ }
60
+ end
61
+
62
+ def connect(consumer, producer)
63
+ @db[:connections].insert({
64
+ consumer_id: consumer.id,
65
+ consumer_type: consumer.type,
66
+ producer_id: producer.id,
67
+ producer_type: producer.type,
68
+ })
69
+ end
70
+
71
+ def produce(event)
72
+ @db[:events].insert({
73
+ producer_type: event.producer.type,
74
+ producer_id: event.producer.id,
75
+ object_type: event.object.type,
76
+ object_id: event.object.id,
77
+ created_at: Time.now.utc,
78
+ })
79
+ end
80
+ end
@@ -0,0 +1,107 @@
1
+ require "helper"
2
+
3
+ class ClientTest < Ag::Test
4
+ def test_initializes_with_adapter
5
+ adapter = Ag::Adapters::Memory.new
6
+ client = Ag::Client.new(adapter)
7
+ assert_instance_of Ag::Client, client
8
+ end
9
+
10
+ def test_forwards_connect_to_adapter
11
+ args = [consumer, producer]
12
+ result = true
13
+ mock_adapter = Minitest::Mock.new
14
+ mock_adapter.expect(:connect, result, args)
15
+
16
+ client = Ag::Client.new(mock_adapter)
17
+ assert_equal result, client.connect(*args)
18
+
19
+ mock_adapter.verify
20
+ end
21
+
22
+ def test_forwards_produce_to_adapter
23
+ args = [event]
24
+ result = [producer]
25
+ mock_adapter = Minitest::Mock.new
26
+ mock_adapter.expect(:produce, result, args)
27
+
28
+ client = Ag::Client.new(mock_adapter)
29
+ assert_equal result, client.produce(*args)
30
+
31
+ mock_adapter.verify
32
+ end
33
+
34
+ def test_forwards_connected_to_adapter
35
+ args = [consumer, producer]
36
+ result = true
37
+ mock_adapter = Minitest::Mock.new
38
+ mock_adapter.expect(:connected?, result, args)
39
+
40
+ client = Ag::Client.new(mock_adapter)
41
+ assert_equal result, client.connected?(*args)
42
+
43
+ mock_adapter.verify
44
+ end
45
+
46
+ def test_forwards_consumers_to_adapter
47
+ args = [producer, {}]
48
+ result = [consumer]
49
+ mock_adapter = Minitest::Mock.new
50
+ mock_adapter.expect(:consumers, result, args)
51
+
52
+ client = Ag::Client.new(mock_adapter)
53
+ assert_equal result, client.consumers(*args)
54
+
55
+ mock_adapter.verify
56
+ end
57
+
58
+ def test_forwards_producers_to_adapter
59
+ args = [consumer, {}]
60
+ result = [producer]
61
+ mock_adapter = Minitest::Mock.new
62
+ mock_adapter.expect(:producers, result, args)
63
+
64
+ client = Ag::Client.new(mock_adapter)
65
+ assert_equal result, client.producers(*args)
66
+
67
+ mock_adapter.verify
68
+ end
69
+
70
+ def test_forwards_timeline_to_adapter
71
+ args = [consumer, {}]
72
+ result = [event]
73
+ mock_adapter = Minitest::Mock.new
74
+ mock_adapter.expect(:timeline, result, args)
75
+
76
+ client = Ag::Client.new(mock_adapter)
77
+ assert_equal result, client.timeline(*args)
78
+
79
+ mock_adapter.verify
80
+ end
81
+
82
+ private
83
+
84
+ def event
85
+ @event ||= Ag::Event.new({
86
+ producer: producer,
87
+ object: object,
88
+ verb: verb,
89
+ })
90
+ end
91
+
92
+ def verb
93
+ "follow"
94
+ end
95
+
96
+ def consumer
97
+ @consumer ||= Ag::Object.new("User", "1")
98
+ end
99
+
100
+ def producer
101
+ @producer ||= Ag::Object.new("User", "1")
102
+ end
103
+
104
+ def object
105
+ @object ||= Ag::Object.new("User", "3")
106
+ end
107
+ end
@@ -0,0 +1,5 @@
1
+ require "minitest/autorun"
2
+ require "ag"
3
+
4
+ class Ag::Test < Minitest::Test
5
+ end
metadata ADDED
@@ -0,0 +1,117 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ag
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - John Nunemaker
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-02-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.7'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.7'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 5.5.1
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 5.5.1
55
+ description: Producers, consumers, connections and timelines.
56
+ email:
57
+ - nunemaker@gmail.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".gitignore"
63
+ - Gemfile
64
+ - LICENSE.txt
65
+ - README.md
66
+ - Rakefile
67
+ - ag.gemspec
68
+ - examples/readme.rb
69
+ - examples/setup.rb
70
+ - lib/ag.rb
71
+ - lib/ag/adapters/memory.rb
72
+ - lib/ag/adapters/sequel_pull.rb
73
+ - lib/ag/adapters/sequel_pull_compact.rb
74
+ - lib/ag/client.rb
75
+ - lib/ag/connection.rb
76
+ - lib/ag/event.rb
77
+ - lib/ag/feed.rb
78
+ - lib/ag/object.rb
79
+ - lib/ag/spec/adapter.rb
80
+ - lib/ag/version.rb
81
+ - script/bootstrap
82
+ - script/test
83
+ - test/adapters/memory_test.rb
84
+ - test/adapters/sequel_pull_compact_test.rb
85
+ - test/adapters/sequel_pull_test.rb
86
+ - test/client_test.rb
87
+ - test/helper.rb
88
+ homepage: ''
89
+ licenses:
90
+ - MIT
91
+ metadata: {}
92
+ post_install_message:
93
+ rdoc_options: []
94
+ require_paths:
95
+ - lib
96
+ required_ruby_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: '0'
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ requirements: []
107
+ rubyforge_project:
108
+ rubygems_version: 2.2.2
109
+ signing_key:
110
+ specification_version: 4
111
+ summary: Producers, consumers, connections and timelines.
112
+ test_files:
113
+ - test/adapters/memory_test.rb
114
+ - test/adapters/sequel_pull_compact_test.rb
115
+ - test/adapters/sequel_pull_test.rb
116
+ - test/client_test.rb
117
+ - test/helper.rb