activehistory 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.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +90 -0
- data/LICENSE +21 -0
- data/README.md +1 -0
- data/Rakefile +12 -0
- data/activehistory.gemspec +40 -0
- data/lib/activehistory.rb +39 -0
- data/lib/activehistory/action.rb +20 -0
- data/lib/activehistory/adapters/active_record.rb +202 -0
- data/lib/activehistory/connection.rb +121 -0
- data/lib/activehistory/event.rb +50 -0
- data/lib/activehistory/exceptions.rb +32 -0
- data/lib/activehistory/regard.rb +11 -0
- data/lib/activehistory/version.rb +3 -0
- data/test/active_record_adapter/association_test/belongs_to_association_test.rb +82 -0
- data/test/active_record_adapter/association_test/has_and_belongs_to_many_test.rb +224 -0
- data/test/active_record_adapter/association_test/has_many_association_test.rb +81 -0
- data/test/active_record_adapter/association_test/has_one_association_test.rb +141 -0
- data/test/active_record_adapter/create_test.rb +58 -0
- data/test/active_record_adapter/destroy_test.rb +38 -0
- data/test/active_record_adapter/event_test.rb +59 -0
- data/test/active_record_adapter/save_test.rb +67 -0
- data/test/factories.rb +36 -0
- data/test/models.rb +60 -0
- data/test/schema.rb +59 -0
- data/test/test_helper.rb +55 -0
- metadata +282 -0
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'net/https'
|
3
|
+
|
4
|
+
class ActiveHistory::Connection
|
5
|
+
|
6
|
+
attr_reader :api_key, :host, :port, :ssl
|
7
|
+
|
8
|
+
def initialize(config)
|
9
|
+
if config[:url]
|
10
|
+
uri = URI.parse(config.delete(:url))
|
11
|
+
config[:api_key] ||= (uri.user ? CGI.unescape(uri.user) : nil)
|
12
|
+
config[:host] ||= uri.host
|
13
|
+
config[:port] ||= uri.port
|
14
|
+
config[:ssl] ||= (uri.scheme == 'https')
|
15
|
+
end
|
16
|
+
|
17
|
+
[:api_key, :host, :port, :ssl, :user_agent].each do |key|
|
18
|
+
self.instance_variable_set(:"@#{key}", config[key])
|
19
|
+
end
|
20
|
+
|
21
|
+
@connection = Net::HTTP.new(host, port)
|
22
|
+
@connection.use_ssl = ssl
|
23
|
+
|
24
|
+
true
|
25
|
+
end
|
26
|
+
|
27
|
+
def user_agent
|
28
|
+
[
|
29
|
+
@user_agent,
|
30
|
+
"Sunstone/#{ActiveHistory::VERSION}",
|
31
|
+
"Ruby/#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}",
|
32
|
+
RUBY_PLATFORM
|
33
|
+
].compact.join(' ')
|
34
|
+
end
|
35
|
+
|
36
|
+
def post(path, body=nil, &block)
|
37
|
+
request = Net::HTTP::Post.new(path)
|
38
|
+
|
39
|
+
send_request(request, body, &block)
|
40
|
+
end
|
41
|
+
|
42
|
+
def url
|
43
|
+
"http#{ssl ? 's' : ''}://#{host}#{port != 80 ? (port == 443 && ssl ? '' : ":#{port}") : ''}"
|
44
|
+
end
|
45
|
+
|
46
|
+
def send_request(request, body=nil, &block)
|
47
|
+
request['Accept'] = 'application/json'
|
48
|
+
request['User-Agent'] = user_agent
|
49
|
+
request['Api-Key'] = api_key
|
50
|
+
request['Content-Type'] = 'application/json'
|
51
|
+
|
52
|
+
if body.is_a?(IO)
|
53
|
+
request['Transfer-Encoding'] = 'chunked'
|
54
|
+
request.body_stream = body
|
55
|
+
elsif body.is_a?(String)
|
56
|
+
request.body = body
|
57
|
+
elsif body
|
58
|
+
request.body = JSON.generate(body)
|
59
|
+
end
|
60
|
+
|
61
|
+
return_value = nil
|
62
|
+
retry_count = 0
|
63
|
+
begin
|
64
|
+
@connection.request(request) do |response|
|
65
|
+
if response['API-Version-Deprecated']
|
66
|
+
logger.warn("DEPRECATION WARNING: API v#{API_VERSION} is being phased out")
|
67
|
+
end
|
68
|
+
|
69
|
+
validate_response_code(response)
|
70
|
+
|
71
|
+
# Get the cookies
|
72
|
+
response.each_header do |key, value|
|
73
|
+
if key.downcase == 'set-cookie' && Thread.current[:sunstone_cookie_store]
|
74
|
+
Thread.current[:sunstone_cookie_store].set_cookie(request_uri, value)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
if block_given?
|
79
|
+
return_value =yield(response)
|
80
|
+
else
|
81
|
+
return_value =response
|
82
|
+
end
|
83
|
+
end
|
84
|
+
rescue ActiveRecord::ConnectionNotEstablished
|
85
|
+
retry_count += 1
|
86
|
+
retry_count == 1 ? retry : raise
|
87
|
+
end
|
88
|
+
|
89
|
+
return_value
|
90
|
+
end
|
91
|
+
|
92
|
+
def validate_response_code(response)
|
93
|
+
code = response.code.to_i
|
94
|
+
|
95
|
+
if !(200..299).include?(code)
|
96
|
+
case code
|
97
|
+
when 400
|
98
|
+
raise ActiveHistory::Exception::BadRequest, response.body
|
99
|
+
when 401
|
100
|
+
raise ActiveHistory::Exception::Unauthorized, response
|
101
|
+
when 404
|
102
|
+
raise ActiveHistory::Exception::NotFound, response
|
103
|
+
when 410
|
104
|
+
raise ActiveHistory::Exception::Gone, response
|
105
|
+
when 422
|
106
|
+
raise ActiveHistory::Exception::ApiVersionUnsupported, response
|
107
|
+
when 503
|
108
|
+
raise ActiveHistory::Exception::ServiceUnavailable, response
|
109
|
+
when 301
|
110
|
+
raise ActiveHistory::Exception::MovedPermanently, response
|
111
|
+
when 502
|
112
|
+
raise ActiveHistory::Exception::BadGateway, response
|
113
|
+
when 300..599
|
114
|
+
raise ActiveHistory::Exception, response
|
115
|
+
else
|
116
|
+
raise ActiveHistory::Exception, response
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
class ActiveHistory::Event
|
2
|
+
|
3
|
+
attr_accessor :actions, :regards, :timestamp
|
4
|
+
|
5
|
+
def initialize(attrs={})
|
6
|
+
@attrs = attrs
|
7
|
+
@attrs[:timestamp] ||= Time.now
|
8
|
+
@actions = []
|
9
|
+
@regards = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def action!(action)
|
13
|
+
action = ActiveHistory::Action.new(action)
|
14
|
+
@actions << action
|
15
|
+
action
|
16
|
+
end
|
17
|
+
|
18
|
+
def action_for(id)
|
19
|
+
@actions.find { |a| a.subject == id }
|
20
|
+
end
|
21
|
+
|
22
|
+
def regard!(regard)
|
23
|
+
@regards << regard
|
24
|
+
end
|
25
|
+
|
26
|
+
def save!
|
27
|
+
return if @actions.empty?
|
28
|
+
|
29
|
+
ActiveHistory.connection.post('/events', self.as_json)
|
30
|
+
end
|
31
|
+
|
32
|
+
def as_json
|
33
|
+
{
|
34
|
+
ip: @attrs[:ip],
|
35
|
+
user_agent: @attrs[:user_agent],
|
36
|
+
session_id: @attrs[:session_id],
|
37
|
+
account: @attrs[:account],
|
38
|
+
api_key: @attrs[:api_key],
|
39
|
+
metadata: @attrs[:metadata],
|
40
|
+
timestamp: @attrs[:timestamp].utc.iso8601(3),
|
41
|
+
actions: @actions.map(&:as_json),
|
42
|
+
regards: @regards.map(&:as_json)
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
def encapsulate
|
47
|
+
ensure
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module ActiveHistory
|
2
|
+
|
3
|
+
class Exception < ::Exception
|
4
|
+
|
5
|
+
class BadGateway < ActiveHistory::Exception
|
6
|
+
end
|
7
|
+
|
8
|
+
class BadRequest < ActiveHistory::Exception
|
9
|
+
end
|
10
|
+
|
11
|
+
class Unauthorized < ActiveHistory::Exception
|
12
|
+
end
|
13
|
+
|
14
|
+
class NotFound < ActiveHistory::Exception
|
15
|
+
end
|
16
|
+
|
17
|
+
class Gone < ActiveHistory::Exception
|
18
|
+
end
|
19
|
+
|
20
|
+
class MovedPermanently < ActiveHistory::Exception
|
21
|
+
end
|
22
|
+
|
23
|
+
class ApiVersionUnsupported < ActiveHistory::Exception
|
24
|
+
end
|
25
|
+
|
26
|
+
class ServiceUnavailable < ActiveHistory::Exception
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class BelongsToAssociationTest < ActiveSupport::TestCase
|
4
|
+
|
5
|
+
def setup
|
6
|
+
super
|
7
|
+
@time = (Time.now.utc + 2).change(usec: 0)
|
8
|
+
end
|
9
|
+
|
10
|
+
test 'TargetModel.create' do
|
11
|
+
@model = create(:account)
|
12
|
+
WebMock::RequestRegistry.instance.reset!
|
13
|
+
|
14
|
+
@target = travel_to(@time) { create(:photo, account: @model) }
|
15
|
+
|
16
|
+
assert_posted("/events") do |req|
|
17
|
+
req_data = JSON.parse(req.body)
|
18
|
+
assert_equal 2, req_data['actions'].size
|
19
|
+
|
20
|
+
assert_equal 'update', req_data['actions'][1]['type']
|
21
|
+
assert_equal "Account/#{@model.id}", req_data['actions'][1]['subject']
|
22
|
+
assert_equal [[], [@target.id]], req_data['actions'][1]['diff']['photo_ids']
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
test 'TargetModel.update removes from association' do
|
27
|
+
@model = create(:account)
|
28
|
+
@target = create(:photo, account: @model)
|
29
|
+
WebMock::RequestRegistry.instance.reset!
|
30
|
+
|
31
|
+
travel_to(@time) { @target.update(account: nil) }
|
32
|
+
|
33
|
+
assert_posted("/events") do |req|
|
34
|
+
req_data = JSON.parse(req.body)
|
35
|
+
assert_equal 2, req_data['actions'].size
|
36
|
+
|
37
|
+
assert_equal 'update', req_data['actions'][1]['type']
|
38
|
+
assert_equal "Account/#{@model.id}", req_data['actions'][1]['subject']
|
39
|
+
assert_equal [[@target.id], []], req_data['actions'][1]['diff']['photo_ids']
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
test 'TargetModel.update changes association' do
|
44
|
+
@model1 = create(:account)
|
45
|
+
@model2 = create(:account)
|
46
|
+
@target = create(:photo, account: @model1)
|
47
|
+
WebMock::RequestRegistry.instance.reset!
|
48
|
+
|
49
|
+
travel_to(@time) { @target.update(account: @model2) }
|
50
|
+
|
51
|
+
assert_posted("/events") do |req|
|
52
|
+
req_data = JSON.parse(req.body)
|
53
|
+
assert_equal 3, req_data['actions'].size
|
54
|
+
|
55
|
+
assert_equal 'update', req_data['actions'][1]['type']
|
56
|
+
assert_equal "Account/#{@model1.id}", req_data['actions'][1]['subject']
|
57
|
+
assert_equal [[@target.id], []], req_data['actions'][1]['diff']['photo_ids']
|
58
|
+
|
59
|
+
assert_equal 'update', req_data['actions'][2]['type']
|
60
|
+
assert_equal "Account/#{@model2.id}", req_data['actions'][2]['subject']
|
61
|
+
assert_equal [[], [@target.id]], req_data['actions'][2]['diff']['photo_ids']
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
test 'TargetModel.destroy removes from association' do
|
66
|
+
@model = create(:account)
|
67
|
+
@target = create(:photo, account: @model)
|
68
|
+
WebMock::RequestRegistry.instance.reset!
|
69
|
+
|
70
|
+
travel_to(@time) { @target.destroy }
|
71
|
+
|
72
|
+
assert_posted("/events") do |req|
|
73
|
+
req_data = JSON.parse(req.body)
|
74
|
+
assert_equal 2, req_data['actions'].size
|
75
|
+
|
76
|
+
assert_equal 'update', req_data['actions'][1]['type']
|
77
|
+
assert_equal "Account/#{@model.id}", req_data['actions'][1]['subject']
|
78
|
+
assert_equal [[@target.id], []], req_data['actions'][1]['diff']['photo_ids']
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
@@ -0,0 +1,224 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class HasAndBelongsToManyAssociationTest < ActiveSupport::TestCase
|
4
|
+
|
5
|
+
def setup
|
6
|
+
super
|
7
|
+
@time = (Time.now.utc + 2).change(usec: 0)
|
8
|
+
end
|
9
|
+
|
10
|
+
test '::create with existing has_and_belongs_to_many association' do
|
11
|
+
@property = create(:property)
|
12
|
+
WebMock::RequestRegistry.instance.reset!
|
13
|
+
|
14
|
+
@region = travel_to(@time) { create(:region, properties: [@property]) }
|
15
|
+
assert_posted("/events") do |req|
|
16
|
+
req_data = JSON.parse(req.body)
|
17
|
+
assert_equal 2, req_data['actions'].size
|
18
|
+
|
19
|
+
assert_equal req_data['actions'][0], {
|
20
|
+
diff: {
|
21
|
+
id: [nil, @region.id],
|
22
|
+
name: [nil, @region.name],
|
23
|
+
property_ids: [[], [@property.id]]
|
24
|
+
},
|
25
|
+
subject: "Region/#{@region.id}",
|
26
|
+
timestamp: @time.iso8601(3),
|
27
|
+
type: 'create'
|
28
|
+
}.as_json
|
29
|
+
|
30
|
+
assert_equal req_data['actions'][1], {
|
31
|
+
timestamp: @time.iso8601(3),
|
32
|
+
type: 'update',
|
33
|
+
subject: "Property/#{@property.id}",
|
34
|
+
diff: {
|
35
|
+
region_ids: [[], [@region.id]]
|
36
|
+
}
|
37
|
+
}.as_json
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
test '::create with new has_and_belongs_to_many association' do
|
42
|
+
WebMock::RequestRegistry.instance.reset!
|
43
|
+
|
44
|
+
@region = travel_to(@time) { create(:region, properties: [build(:property)]) }
|
45
|
+
@property = @region.properties.first
|
46
|
+
|
47
|
+
assert_posted("/events") do |req|
|
48
|
+
req_data = JSON.parse(req.body)
|
49
|
+
assert_equal 2, req_data['actions'].size
|
50
|
+
|
51
|
+
assert_equal req_data['actions'][0], {
|
52
|
+
diff: {
|
53
|
+
id: [nil, @region.id],
|
54
|
+
name: [nil, @region.name],
|
55
|
+
property_ids: [[], [@property.id]]
|
56
|
+
},
|
57
|
+
subject: "Region/#{@region.id}",
|
58
|
+
timestamp: @time.iso8601(3),
|
59
|
+
type: 'create'
|
60
|
+
}.as_json
|
61
|
+
|
62
|
+
assert_equal req_data['actions'][1], {
|
63
|
+
timestamp: @time.iso8601(3),
|
64
|
+
type: 'create',
|
65
|
+
subject: "Property/#{@property.id}",
|
66
|
+
diff: {
|
67
|
+
id: [nil, @property.id],
|
68
|
+
name: [nil, @property.name],
|
69
|
+
description: [nil, @property.description],
|
70
|
+
constructed: [nil, @property.constructed],
|
71
|
+
size: [nil, @property.size],
|
72
|
+
created_at: [nil, @property.created_at],
|
73
|
+
aliases: [nil, []],
|
74
|
+
active: [nil, @property.active],
|
75
|
+
region_ids: [[], [@region.id]]
|
76
|
+
}
|
77
|
+
}.as_json
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
test '::update with adding existing has_and_belongs_to_many association' do
|
82
|
+
@property = create(:property)
|
83
|
+
@region = create(:region)
|
84
|
+
WebMock::RequestRegistry.instance.reset!
|
85
|
+
|
86
|
+
travel_to(@time) { @region.update(properties: [@property]) }
|
87
|
+
|
88
|
+
assert_posted("/events") do |req|
|
89
|
+
req_data = JSON.parse(req.body)
|
90
|
+
assert_equal 2, req_data['actions'].size
|
91
|
+
|
92
|
+
assert_equal req_data['actions'][0], {
|
93
|
+
diff: {
|
94
|
+
property_ids: [[], [@property.id]]
|
95
|
+
},
|
96
|
+
subject: "Region/#{@region.id}",
|
97
|
+
timestamp: @time.iso8601(3),
|
98
|
+
type: 'update'
|
99
|
+
}.as_json
|
100
|
+
|
101
|
+
assert_equal req_data['actions'][1], {
|
102
|
+
timestamp: @time.iso8601(3),
|
103
|
+
type: 'update',
|
104
|
+
subject: "Property/#{@property.id}",
|
105
|
+
diff: {
|
106
|
+
region_ids: [[], [@region.id]]
|
107
|
+
}
|
108
|
+
}.as_json
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
test '::update with adding new has_and_belongs_to_many association' do
|
113
|
+
@region = create(:region)
|
114
|
+
WebMock::RequestRegistry.instance.reset!
|
115
|
+
|
116
|
+
travel_to(@time) { @region.update(properties: [build(:property)]) }
|
117
|
+
@property = @region.properties.first
|
118
|
+
|
119
|
+
assert_posted("/events") do |req|
|
120
|
+
req_data = JSON.parse(req.body)
|
121
|
+
assert_equal 2, req_data['actions'].size
|
122
|
+
|
123
|
+
assert_equal req_data['actions'][0], {
|
124
|
+
timestamp: @time.iso8601(3),
|
125
|
+
type: 'create',
|
126
|
+
subject: "Property/#{@property.id}",
|
127
|
+
diff: {
|
128
|
+
id: [nil, @property.id],
|
129
|
+
name: [nil, @property.name],
|
130
|
+
description: [nil, @property.description],
|
131
|
+
constructed: [nil, @property.constructed],
|
132
|
+
size: [nil, @property.size],
|
133
|
+
created_at: [nil, @property.created_at],
|
134
|
+
aliases: [nil, []],
|
135
|
+
active: [nil, @property.active],
|
136
|
+
region_ids: [[], [@region.id]]
|
137
|
+
}
|
138
|
+
}.as_json
|
139
|
+
|
140
|
+
assert_equal req_data['actions'][1], {
|
141
|
+
diff: {
|
142
|
+
property_ids: [[], [@property.id]]
|
143
|
+
},
|
144
|
+
subject: "Region/#{@region.id}",
|
145
|
+
timestamp: @time.iso8601(3),
|
146
|
+
type: 'update'
|
147
|
+
}.as_json
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
test '::update with removing has_and_belongs_to_many association' do
|
152
|
+
@property = create(:property)
|
153
|
+
@region = create(:region, properties: [@property])
|
154
|
+
WebMock::RequestRegistry.instance.reset!
|
155
|
+
|
156
|
+
travel_to(@time) { @region.update(properties: []) }
|
157
|
+
|
158
|
+
assert_posted("/events") do |req|
|
159
|
+
req_data = JSON.parse(req.body)
|
160
|
+
assert_equal 2, req_data['actions'].size
|
161
|
+
|
162
|
+
assert_equal req_data['actions'][0], {
|
163
|
+
diff: {
|
164
|
+
property_ids: [[@property.id], []]
|
165
|
+
},
|
166
|
+
subject: "Region/#{@region.id}",
|
167
|
+
timestamp: @time.iso8601(3),
|
168
|
+
type: 'update'
|
169
|
+
}.as_json
|
170
|
+
|
171
|
+
assert_equal req_data['actions'][1], {
|
172
|
+
timestamp: @time.iso8601(3),
|
173
|
+
type: 'update',
|
174
|
+
subject: "Property/#{@property.id}",
|
175
|
+
diff: {
|
176
|
+
region_ids: [[@region.id], []]
|
177
|
+
}
|
178
|
+
}.as_json
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
test '::destroying updates has_and_belongs_to_many associations' do
|
183
|
+
@property = create(:property)
|
184
|
+
@region = create(:region, properties: [@property])
|
185
|
+
WebMock::RequestRegistry.instance.reset!
|
186
|
+
|
187
|
+
travel_to(@time) { @region.destroy }
|
188
|
+
|
189
|
+
assert_posted("/events") do |req|
|
190
|
+
req_data = JSON.parse(req.body)
|
191
|
+
assert_equal 2, req_data['actions'].size
|
192
|
+
|
193
|
+
assert_equal req_data['actions'][0], {
|
194
|
+
diff: {
|
195
|
+
id: [@region.id, nil],
|
196
|
+
name: [@region.name, nil],
|
197
|
+
property_ids: [[@property.id], []]
|
198
|
+
},
|
199
|
+
subject: "Region/#{@region.id}",
|
200
|
+
timestamp: @time.iso8601(3),
|
201
|
+
type: 'destroy'
|
202
|
+
}.as_json
|
203
|
+
|
204
|
+
assert_equal req_data['actions'][1], {
|
205
|
+
timestamp: @time.iso8601(3),
|
206
|
+
type: 'update',
|
207
|
+
subject: "Property/#{@property.id}",
|
208
|
+
diff: {
|
209
|
+
region_ids: [[@region.id], []]
|
210
|
+
}
|
211
|
+
}.as_json
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
|
216
|
+
test 'has_and_belongs_to_many <<'
|
217
|
+
test 'has_and_belongs_to_many.delete'
|
218
|
+
test 'has_and_belongs_to_many.destroy'
|
219
|
+
test 'has_and_belongs_to_many='
|
220
|
+
test 'has_and_belongs_to_many_ids='
|
221
|
+
test 'has_and_belongs_to_many.clear'
|
222
|
+
test 'has_and_belongs_to_many.create'
|
223
|
+
|
224
|
+
end
|