ag 0.0.1

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,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