tessa 0.1.3 → 0.2.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.
- checksums.yaml +4 -4
- data/lib/tessa.rb +13 -1
- data/lib/tessa/asset_change.rb +49 -0
- data/lib/tessa/asset_change_set.rb +49 -0
- data/lib/tessa/model.rb +78 -0
- data/lib/tessa/model/field.rb +77 -0
- data/lib/tessa/response_factory.rb +1 -1
- data/lib/tessa/version.rb +1 -1
- data/spec/support/remote_call_macro.rb +4 -1
- data/spec/tessa/asset_change_set_spec.rb +198 -0
- data/spec/tessa/asset_change_spec.rb +86 -0
- data/spec/tessa/model_field_spec.rb +72 -0
- data/spec/tessa/model_spec.rb +366 -0
- metadata +14 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a4842f3250777ed9f020f3096a04782f1e740a7d
|
4
|
+
data.tar.gz: a1984882ebc543a5a0acf098f54b4c081f61b8b0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ed5894431e9139b5440edb4d5cf30ad8c2f4364771953215a4e60089076e615b910be1bbb43616b35f9fa0a62e1b7b3dba893d06f4623dd59bcf6715e0da0db0
|
7
|
+
data.tar.gz: 5c1d7e34c98f0bc52a7bc01354217be0669346d97eeb1f2a71c2dcd5eb37aaecd9ab320d7f13e513dbba370960e65e23bb2062ea5c735823f12b751b012b9824
|
data/lib/tessa.rb
CHANGED
@@ -8,8 +8,12 @@ require "json"
|
|
8
8
|
require "tessa/config"
|
9
9
|
require "tessa/response_factory"
|
10
10
|
require "tessa/asset"
|
11
|
+
require "tessa/asset_change"
|
12
|
+
require "tessa/asset_change_set"
|
13
|
+
require "tessa/model"
|
11
14
|
require "tessa/upload"
|
12
15
|
|
16
|
+
|
13
17
|
module Tessa
|
14
18
|
def self.config
|
15
19
|
@config ||= Config.new
|
@@ -19,5 +23,13 @@ module Tessa
|
|
19
23
|
yield config
|
20
24
|
end
|
21
25
|
|
22
|
-
class RequestFailed < StandardError
|
26
|
+
class RequestFailed < StandardError
|
27
|
+
attr_reader :response
|
28
|
+
|
29
|
+
def initialize(message=nil, response=nil)
|
30
|
+
super(message)
|
31
|
+
@response = response
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
23
35
|
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Tessa
|
2
|
+
class AssetChange
|
3
|
+
include Virtus.model
|
4
|
+
|
5
|
+
attribute :id, Integer
|
6
|
+
attribute :action, String
|
7
|
+
|
8
|
+
def initialize(args={})
|
9
|
+
case args
|
10
|
+
when Array
|
11
|
+
id, attributes = args
|
12
|
+
super attributes.merge(id: id)
|
13
|
+
else
|
14
|
+
super
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def apply
|
19
|
+
if add?
|
20
|
+
asset.complete!
|
21
|
+
elsif remove?
|
22
|
+
asset.delete!
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def hash
|
27
|
+
[id, action].hash
|
28
|
+
end
|
29
|
+
|
30
|
+
def eql?(b)
|
31
|
+
self.class == b.class &&
|
32
|
+
self.hash == b.hash
|
33
|
+
end
|
34
|
+
|
35
|
+
def add?
|
36
|
+
action == 'add'
|
37
|
+
end
|
38
|
+
|
39
|
+
def remove?
|
40
|
+
action == 'remove'
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def asset
|
46
|
+
Tessa::Asset.new(id: id)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Tessa
|
2
|
+
class AssetChangeSet
|
3
|
+
include Virtus.model
|
4
|
+
|
5
|
+
attribute :changes, Array[AssetChange]
|
6
|
+
attribute :scoped_ids, Array[Integer]
|
7
|
+
|
8
|
+
def scoped_ids=(new_ids)
|
9
|
+
super new_ids.compact
|
10
|
+
end
|
11
|
+
|
12
|
+
def scoped_changes
|
13
|
+
changes.select { |change| scoped_ids.include?(change.id) }
|
14
|
+
end
|
15
|
+
|
16
|
+
def apply
|
17
|
+
scoped_changes.uniq.each(&:apply)
|
18
|
+
end
|
19
|
+
|
20
|
+
def +(b)
|
21
|
+
self.changes = (self.changes + b.changes).uniq
|
22
|
+
self.scoped_ids = (self.scoped_ids + b.scoped_ids).uniq
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
def add(value)
|
27
|
+
id = id_from_asset(value)
|
28
|
+
changes << AssetChange.new(id: id, action: "add")
|
29
|
+
scoped_ids << id
|
30
|
+
end
|
31
|
+
|
32
|
+
def remove(value)
|
33
|
+
id = id_from_asset(value)
|
34
|
+
changes << AssetChange.new(id: id, action: "remove")
|
35
|
+
scoped_ids << id
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def id_from_asset(value)
|
41
|
+
case value
|
42
|
+
when Asset
|
43
|
+
value.id
|
44
|
+
when Fixnum
|
45
|
+
value
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/tessa/model.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'tessa/model/field'
|
2
|
+
|
3
|
+
module Tessa
|
4
|
+
module Model
|
5
|
+
|
6
|
+
def self.included(base)
|
7
|
+
base.send :include, InstanceMethods
|
8
|
+
base.extend ClassMethods
|
9
|
+
base.after_commit :apply_tessa_change_sets if base.respond_to?(:after_commit)
|
10
|
+
base.before_destroy :remove_all_tessa_assets if base.respond_to?(:before_destroy)
|
11
|
+
end
|
12
|
+
|
13
|
+
module InstanceMethods
|
14
|
+
|
15
|
+
def pending_tessa_change_sets
|
16
|
+
@pending_tessa_change_sets ||= Hash.new { AssetChangeSet.new }
|
17
|
+
end
|
18
|
+
|
19
|
+
def apply_tessa_change_sets
|
20
|
+
@pending_tessa_change_sets.delete_if do |_, change_set|
|
21
|
+
change_set.apply
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def remove_all_tessa_assets
|
26
|
+
self.class.tessa_fields.each do |name, field|
|
27
|
+
change_set = pending_tessa_change_sets[name]
|
28
|
+
field.ids(on: self).each do |asset_id|
|
29
|
+
change_set.remove(asset_id)
|
30
|
+
end
|
31
|
+
pending_tessa_change_sets[name] = change_set
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
module ClassMethods
|
38
|
+
|
39
|
+
def asset(name, args={})
|
40
|
+
field = tessa_fields[name] = Field.new(args.merge(name: name))
|
41
|
+
|
42
|
+
define_method(name) do
|
43
|
+
if instance_variable_defined?(ivar = "@#{name}")
|
44
|
+
instance_variable_get(ivar)
|
45
|
+
else
|
46
|
+
instance_variable_set(
|
47
|
+
ivar,
|
48
|
+
Tessa::Asset.find(field.id(on: self))
|
49
|
+
)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
define_method("#{name}=") do |value|
|
54
|
+
change_set = field.change_set_for(value)
|
55
|
+
|
56
|
+
if !(field.multiple? && value.is_a?(AssetChangeSet))
|
57
|
+
new_ids = change_set.scoped_changes.select(&:add?).map(&:id)
|
58
|
+
change_set += field.difference_change_set(new_ids, on: self)
|
59
|
+
end
|
60
|
+
|
61
|
+
pending_tessa_change_sets[name] += change_set
|
62
|
+
|
63
|
+
field.apply(change_set, on: self)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def tessa_fields
|
68
|
+
@tessa_fields ||= {}
|
69
|
+
end
|
70
|
+
|
71
|
+
def inherited(subclass)
|
72
|
+
subclass.instance_variable_set(:@tessa_fields, @tessa_fields.dup)
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module Tessa
|
2
|
+
module Model
|
3
|
+
class Field
|
4
|
+
include Virtus.model
|
5
|
+
|
6
|
+
attribute :model
|
7
|
+
attribute :name, String
|
8
|
+
attribute :multiple, Boolean, default: false
|
9
|
+
attribute :id_field, String
|
10
|
+
|
11
|
+
def id_field
|
12
|
+
super || "#{name}#{default_id_field_suffix}"
|
13
|
+
end
|
14
|
+
|
15
|
+
def ids(on:)
|
16
|
+
[*id(on: on)]
|
17
|
+
end
|
18
|
+
|
19
|
+
def id(on:)
|
20
|
+
on.public_send(id_field)
|
21
|
+
end
|
22
|
+
|
23
|
+
def apply(set, on:)
|
24
|
+
ids = ids(on: on)
|
25
|
+
|
26
|
+
set.scoped_changes.each do |change|
|
27
|
+
if change.add?
|
28
|
+
ids << change.id
|
29
|
+
elsif change.remove?
|
30
|
+
ids.delete change.id
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
if multiple?
|
35
|
+
on.public_send(id_field_writer, ids)
|
36
|
+
else
|
37
|
+
on.public_send(id_field_writer, ids.first)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def change_set_for(value)
|
42
|
+
case value
|
43
|
+
when AssetChangeSet
|
44
|
+
value
|
45
|
+
when Array
|
46
|
+
value.map { |item| change_set_for(item) }.reduce(:+)
|
47
|
+
when Asset
|
48
|
+
AssetChangeSet.new.tap { |set| set.add(value) }
|
49
|
+
else
|
50
|
+
AssetChangeSet.new
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def difference_change_set(subtrahend_ids, on:)
|
55
|
+
AssetChangeSet.new.tap do |change_set|
|
56
|
+
(ids(on: on) - subtrahend_ids).each do |id|
|
57
|
+
change_set.remove(id)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def id_field_writer
|
65
|
+
"#{id_field}="
|
66
|
+
end
|
67
|
+
|
68
|
+
def default_id_field_suffix
|
69
|
+
if multiple
|
70
|
+
"_ids"
|
71
|
+
else
|
72
|
+
"_id"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -2,7 +2,7 @@ module Tessa
|
|
2
2
|
module ResponseFactory
|
3
3
|
|
4
4
|
def new_from_response(response)
|
5
|
-
raise RequestFailed unless response.success?
|
5
|
+
raise RequestFailed.new("Tessa responded with #{response.status}", response) unless response.success?
|
6
6
|
case json = JSON.parse(response.body)
|
7
7
|
when Array
|
8
8
|
json.map { |record| new record }
|
data/lib/tessa/version.rb
CHANGED
@@ -31,7 +31,10 @@ RSpec.shared_examples_for "remote call macro" do |method, path, return_type|
|
|
31
31
|
}
|
32
32
|
|
33
33
|
it "raises Tessa::RequestFailed" do
|
34
|
-
expect{ call }.to raise_error
|
34
|
+
expect{ call }.to raise_error { |error|
|
35
|
+
expect(error).to be_a(Tessa::RequestFailed)
|
36
|
+
expect(error.response).to be_a(Faraday::Response)
|
37
|
+
}
|
35
38
|
end
|
36
39
|
end
|
37
40
|
|
@@ -0,0 +1,198 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Tessa::AssetChangeSet do
|
4
|
+
subject(:set) { described_class.new(args) }
|
5
|
+
let(:args) {
|
6
|
+
{
|
7
|
+
changes: Array.new(2) { Tessa::AssetChange.new },
|
8
|
+
scoped_ids: [1, 2],
|
9
|
+
}
|
10
|
+
}
|
11
|
+
|
12
|
+
describe "#initialize" do
|
13
|
+
it "sets :changes to attribute" do
|
14
|
+
expect(set.changes).to eq(args[:changes])
|
15
|
+
end
|
16
|
+
|
17
|
+
it "sets :scoped_ids to attribute" do
|
18
|
+
expect(set.scoped_ids).to eq([1, 2])
|
19
|
+
end
|
20
|
+
|
21
|
+
context "with various scoped_id input types" do
|
22
|
+
before { args[:scoped_ids] = ["1", 2, nil] }
|
23
|
+
|
24
|
+
it "ensures all values are integers and ignores nils" do
|
25
|
+
expect(set.scoped_ids).to eq([1, 2])
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context "with hash format for changes" do
|
30
|
+
before do
|
31
|
+
args[:changes] = { "123" => { "action" => "add" }, "456" => { "action" => "remove" } }
|
32
|
+
end
|
33
|
+
|
34
|
+
it "initializes AssetChange objects for each hash" do
|
35
|
+
expect(set.changes[0].id).to eq(123)
|
36
|
+
expect(set.changes[0].action).to eq("add")
|
37
|
+
expect(set.changes[1].id).to eq(456)
|
38
|
+
expect(set.changes[1].action).to eq("remove")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe "#scoped_changes" do
|
44
|
+
context "with changes not contained in scoped ids" do
|
45
|
+
let(:args) {
|
46
|
+
{
|
47
|
+
changes: [
|
48
|
+
Tessa::AssetChange.new(id: 1),
|
49
|
+
Tessa::AssetChange.new(id: 2),
|
50
|
+
Tessa::AssetChange.new(id: 3),
|
51
|
+
Tessa::AssetChange.new(id: 4)],
|
52
|
+
scoped_ids: [1, 2],
|
53
|
+
}
|
54
|
+
}
|
55
|
+
|
56
|
+
it "only returns the scoped changes" do
|
57
|
+
expect(set.scoped_changes.size).to eq(2)
|
58
|
+
expect(set.scoped_changes.map(&:id)).to eq([1, 2])
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe "#apply" do
|
64
|
+
|
65
|
+
context "with unscoped changes" do
|
66
|
+
let(:args) {
|
67
|
+
{
|
68
|
+
changes: [
|
69
|
+
Tessa::AssetChange.new(id: 1),
|
70
|
+
Tessa::AssetChange.new(id: 2)],
|
71
|
+
scoped_ids: [1],
|
72
|
+
}
|
73
|
+
}
|
74
|
+
|
75
|
+
it "calls apply on each scoped change" do
|
76
|
+
expect(set.changes[0]).to receive(:apply)
|
77
|
+
expect(set.changes[1]).not_to receive(:apply)
|
78
|
+
set.apply
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
context "with duplicate changes" do
|
83
|
+
let(:args) {
|
84
|
+
{
|
85
|
+
changes: [
|
86
|
+
Tessa::AssetChange.new(id: 1, action: 'remove'),
|
87
|
+
Tessa::AssetChange.new(id: 1, action: 'remove')],
|
88
|
+
scoped_ids: [1],
|
89
|
+
}
|
90
|
+
}
|
91
|
+
|
92
|
+
it "only calls apply on unique elements" do
|
93
|
+
expect(set.changes[0]).to receive(:apply)
|
94
|
+
expect(set.changes[1]).not_to receive(:apply)
|
95
|
+
set.apply
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
describe "#+" do
|
101
|
+
let(:a) {
|
102
|
+
described_class.new(
|
103
|
+
changes: [Tessa::AssetChange.new(id: 1, action: 'add')],
|
104
|
+
scoped_ids: [1],
|
105
|
+
)
|
106
|
+
}
|
107
|
+
let(:b) {
|
108
|
+
described_class.new(
|
109
|
+
changes: [Tessa::AssetChange.new(id: 2, action: 'add')],
|
110
|
+
scoped_ids: [2],
|
111
|
+
)
|
112
|
+
}
|
113
|
+
subject(:sum) { a + b }
|
114
|
+
|
115
|
+
it "concatenates the values of changes" do
|
116
|
+
expect(sum.changes.collect(&:id)).to eq([1, 2])
|
117
|
+
end
|
118
|
+
|
119
|
+
it "concatenates the values of scoped_ids" do
|
120
|
+
expect(sum.scoped_ids).to eq([1, 2])
|
121
|
+
end
|
122
|
+
|
123
|
+
context "with duplicate entries" do
|
124
|
+
subject(:sum) { a + b + b }
|
125
|
+
|
126
|
+
it "only includes unique values" do
|
127
|
+
expect(sum.changes.collect(&:id)).to eq([1, 2])
|
128
|
+
expect(sum.scoped_ids).to eq([1, 2])
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
describe "#add" do
|
134
|
+
let(:args) { {} }
|
135
|
+
let(:asset) { Tessa::Asset.new(id: 1) }
|
136
|
+
|
137
|
+
shared_examples_for "adds the represented asset" do
|
138
|
+
it "adds an 'add' change for this asset" do
|
139
|
+
expect(set.changes[0].id).to eq(1)
|
140
|
+
expect(set.changes[0].action).to eq("add")
|
141
|
+
end
|
142
|
+
|
143
|
+
it "adds the asset id to the scoped_ids array" do
|
144
|
+
expect(set.scoped_ids).to eq([1])
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
context "when passed an asset" do
|
149
|
+
before do
|
150
|
+
set.add(asset)
|
151
|
+
end
|
152
|
+
|
153
|
+
it_behaves_like "adds the represented asset"
|
154
|
+
end
|
155
|
+
|
156
|
+
context "when passed an integer id" do
|
157
|
+
before do
|
158
|
+
set.add(1)
|
159
|
+
end
|
160
|
+
|
161
|
+
it_behaves_like "adds the represented asset"
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
describe "#remove" do
|
166
|
+
let(:args) { {} }
|
167
|
+
let(:asset) { Tessa::Asset.new(id: 1) }
|
168
|
+
|
169
|
+
shared_examples_for "removes the represented asset" do
|
170
|
+
it "adds a 'remove' change for this asset" do
|
171
|
+
expect(set.changes[0].id).to eq(1)
|
172
|
+
expect(set.changes[0].action).to eq("remove")
|
173
|
+
end
|
174
|
+
|
175
|
+
it "adds the asset id to the scoped_ids array" do
|
176
|
+
expect(set.scoped_ids).to eq([1])
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
context "when passed an asset" do
|
181
|
+
before do
|
182
|
+
set.remove(asset)
|
183
|
+
end
|
184
|
+
|
185
|
+
it_behaves_like "removes the represented asset"
|
186
|
+
end
|
187
|
+
|
188
|
+
context "when passed an integer id" do
|
189
|
+
before do
|
190
|
+
set.remove(1)
|
191
|
+
end
|
192
|
+
|
193
|
+
it_behaves_like "removes the represented asset"
|
194
|
+
end
|
195
|
+
|
196
|
+
end
|
197
|
+
|
198
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Tessa::AssetChange do
|
4
|
+
subject(:change) { described_class.new(args) }
|
5
|
+
let(:args) {
|
6
|
+
{
|
7
|
+
id: 123,
|
8
|
+
action: "add",
|
9
|
+
}
|
10
|
+
}
|
11
|
+
|
12
|
+
describe "#initialize" do
|
13
|
+
context "with hash args" do
|
14
|
+
it "sets id" do
|
15
|
+
expect(subject.id).to eq(123)
|
16
|
+
end
|
17
|
+
|
18
|
+
it "sets action" do
|
19
|
+
expect(subject.action).to eq("add")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
context "with array arg" do
|
24
|
+
let(:args) { [123, { "action" => "add" }] }
|
25
|
+
|
26
|
+
it "sets id" do
|
27
|
+
expect(subject.id).to eq(123)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "sets action" do
|
31
|
+
expect(subject.action).to eq("add")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe "#apply" do
|
37
|
+
let(:asset) { instance_spy(Tessa::Asset) }
|
38
|
+
before do
|
39
|
+
expect(Tessa::Asset).to receive(:new).with(id: 123).and_return(asset)
|
40
|
+
end
|
41
|
+
|
42
|
+
context "with action 'add'" do
|
43
|
+
before { args[:action] = "add" }
|
44
|
+
it "calls complete! on asset with :id" do
|
45
|
+
change.apply
|
46
|
+
expect(asset).to have_received(:complete!)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context "with action 'remove'" do
|
51
|
+
before { args[:action] = "remove" }
|
52
|
+
it "calls delete! on asset with :id" do
|
53
|
+
change.apply
|
54
|
+
expect(asset).to have_received(:delete!)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe "#add?" do
|
60
|
+
subject(:add?) { change.add? }
|
61
|
+
|
62
|
+
context "action = 'add'" do
|
63
|
+
before { change.action = 'add' }
|
64
|
+
it { is_expected.to eq(true) }
|
65
|
+
end
|
66
|
+
|
67
|
+
context "action != 'add'" do
|
68
|
+
before { change.action = 'something' }
|
69
|
+
it { is_expected.to eq(false) }
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe "#remove?" do
|
74
|
+
subject(:remove?) { change.remove? }
|
75
|
+
|
76
|
+
context "action = 'remove'" do
|
77
|
+
before { change.action = 'remove' }
|
78
|
+
it { is_expected.to eq(true) }
|
79
|
+
end
|
80
|
+
|
81
|
+
context "action != 'remove'" do
|
82
|
+
before { change.action = 'something' }
|
83
|
+
it { is_expected.to eq(false) }
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Tessa::Model::Field do
|
4
|
+
subject(:field) { described_class.new(attrs) }
|
5
|
+
let(:attrs) { {} }
|
6
|
+
|
7
|
+
describe "#initialize" do
|
8
|
+
context "with all fields set" do
|
9
|
+
let(:attrs) {
|
10
|
+
{
|
11
|
+
model: :model,
|
12
|
+
name: "name",
|
13
|
+
multiple: true,
|
14
|
+
id_field: "my_field",
|
15
|
+
}
|
16
|
+
}
|
17
|
+
|
18
|
+
it "sets :model to attribute" do
|
19
|
+
expect(field.model).to eq(:model)
|
20
|
+
end
|
21
|
+
|
22
|
+
it "sets :name to attribute" do
|
23
|
+
expect(field.name).to eq("name")
|
24
|
+
end
|
25
|
+
|
26
|
+
it "sets :multiple to attribute" do
|
27
|
+
expect(field.multiple).to eq(true)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "sets :id_field to attribute" do
|
31
|
+
expect(field.id_field).to eq("my_field")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context "with no fields set" do
|
36
|
+
it "defaults model to nil" do
|
37
|
+
expect(field.model).to be_nil
|
38
|
+
end
|
39
|
+
|
40
|
+
it "defaults name to nil" do
|
41
|
+
expect(field.name).to be_nil
|
42
|
+
end
|
43
|
+
|
44
|
+
it "defaults multiple to false" do
|
45
|
+
expect(field.multiple).to eq(false)
|
46
|
+
end
|
47
|
+
|
48
|
+
context "when multiple true" do
|
49
|
+
before do
|
50
|
+
attrs[:name] = "my_name"
|
51
|
+
attrs[:multiple] = false
|
52
|
+
end
|
53
|
+
|
54
|
+
it "defaults id_field to name + '_id'" do
|
55
|
+
expect(field.id_field).to eq("my_name_id")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
context "when multiple false" do
|
60
|
+
before do
|
61
|
+
attrs[:name] = "my_name"
|
62
|
+
attrs[:multiple] = true
|
63
|
+
end
|
64
|
+
|
65
|
+
it "defaults id_field to name + '_ids'" do
|
66
|
+
expect(field.id_field).to eq("my_name_ids")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
@@ -0,0 +1,366 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Tessa::Model do
|
4
|
+
subject(:described_module) { described_class }
|
5
|
+
let(:model) { Class.new.tap { |c| c.send(:include, described_module) } }
|
6
|
+
|
7
|
+
it { is_expected.to be_a(Module) }
|
8
|
+
|
9
|
+
describe "::asset" do
|
10
|
+
it "creates ModelField and sets it by name to @tessa_fields" do
|
11
|
+
model.asset :new_field
|
12
|
+
expect(model.tessa_fields[:new_field]).to be_a(Tessa::Model::Field)
|
13
|
+
end
|
14
|
+
|
15
|
+
context "with a field named :avatar" do
|
16
|
+
subject(:instance) { model.new }
|
17
|
+
before do
|
18
|
+
model.asset :avatar
|
19
|
+
end
|
20
|
+
|
21
|
+
it "creates an #avatar method" do
|
22
|
+
expect(instance).to respond_to(:avatar)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "creates an #avatar= method" do
|
26
|
+
expect(instance).to respond_to(:avatar=)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context "with customized field" do
|
31
|
+
before do
|
32
|
+
model.asset :custom_field, multiple: true, id_field: "another_place"
|
33
|
+
end
|
34
|
+
|
35
|
+
it "sets all attributes on ModelField properly" do
|
36
|
+
field = model.tessa_fields[:custom_field]
|
37
|
+
expect(field.name).to eq("custom_field")
|
38
|
+
expect(field.multiple).to eq(true)
|
39
|
+
expect(field.id_field).to eq("another_place")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context "with inheritance hierarchy" do
|
44
|
+
let(:submodel) { Class.new(model) }
|
45
|
+
before do
|
46
|
+
model.asset :field1
|
47
|
+
submodel.asset :field2
|
48
|
+
model.asset :field3
|
49
|
+
end
|
50
|
+
|
51
|
+
it "submodel has its own list of fields" do
|
52
|
+
expect(submodel.tessa_fields.keys).to eq([:field1, :field2])
|
53
|
+
end
|
54
|
+
|
55
|
+
it "does not alter parent class fields" do
|
56
|
+
expect(model.tessa_fields.keys).to eq([:field1, :field3])
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe "#asset_setter" do
|
62
|
+
let(:instance) { model.new }
|
63
|
+
|
64
|
+
context "with change set present for a field" do
|
65
|
+
let(:a) { Tessa::Asset.new(id: 1) }
|
66
|
+
let(:b) { Tessa::Asset.new(id: 2) }
|
67
|
+
let(:set) { instance.pending_tessa_change_sets[:field] }
|
68
|
+
before do
|
69
|
+
model.send :attr_accessor, :field_id
|
70
|
+
model.asset :field
|
71
|
+
instance.field = a
|
72
|
+
end
|
73
|
+
|
74
|
+
it "combines the changes with the new set" do
|
75
|
+
instance.field = b
|
76
|
+
instance.field = nil
|
77
|
+
changes = set.changes.map { |change| [change.id, change.action.to_sym] }
|
78
|
+
expect(changes).to eq([
|
79
|
+
[1, :add],
|
80
|
+
[2, :add],
|
81
|
+
[1, :remove],
|
82
|
+
[2, :remove],
|
83
|
+
])
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
context "with a multiple typed field" do
|
88
|
+
before do
|
89
|
+
model.send(:attr_accessor, :file_ids)
|
90
|
+
model.asset :file, multiple: true
|
91
|
+
end
|
92
|
+
|
93
|
+
context "when set with array of assets" do
|
94
|
+
let(:assets) {
|
95
|
+
[
|
96
|
+
Tessa::Asset.new(id: 1),
|
97
|
+
Tessa::Asset.new(id: 2),
|
98
|
+
]
|
99
|
+
}
|
100
|
+
|
101
|
+
it "sets field to list of ids from assets" do
|
102
|
+
instance.file = assets
|
103
|
+
expect(instance.file_ids).to eq([1, 2])
|
104
|
+
end
|
105
|
+
|
106
|
+
it "removes any ids that aren't in list" do
|
107
|
+
instance.file_ids = [3]
|
108
|
+
instance.file = assets
|
109
|
+
expect(instance.file_ids).to eq([1, 2])
|
110
|
+
end
|
111
|
+
|
112
|
+
it "adds an AssetChangeSet to pending queue for this field" do
|
113
|
+
instance.file = assets
|
114
|
+
expect(instance.pending_tessa_change_sets).to be_a(Hash)
|
115
|
+
expect(instance.pending_tessa_change_sets[:file]).to be_a(Tessa::AssetChangeSet)
|
116
|
+
end
|
117
|
+
|
118
|
+
describe "the added change set" do
|
119
|
+
subject(:set) { instance.pending_tessa_change_sets[:file] }
|
120
|
+
|
121
|
+
it "has an 'add' action for each new asset" do
|
122
|
+
instance.file = assets
|
123
|
+
ids = set.changes.select { |c| c.action == 'add' }.collect(&:id)
|
124
|
+
expect(ids).to eq([1, 2])
|
125
|
+
end
|
126
|
+
|
127
|
+
it "has a 'remove' action for each missing asset" do
|
128
|
+
instance.file_ids = [3]
|
129
|
+
instance.file = assets
|
130
|
+
ids = set.changes.select { |c| c.action == 'remove' }.collect(&:id)
|
131
|
+
expect(ids).to eq([3])
|
132
|
+
end
|
133
|
+
|
134
|
+
it "adds each of the ids to the scoped_ids list" do
|
135
|
+
instance.file_ids = [3]
|
136
|
+
instance.file = assets
|
137
|
+
expect(set.scoped_ids).to eq([1, 2, 3])
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
context "when set with an AssetChangeSet" do
|
143
|
+
let(:set) { Tessa::AssetChangeSet.new }
|
144
|
+
before do
|
145
|
+
set.add(1)
|
146
|
+
set.add(2)
|
147
|
+
set.changes << Tessa::AssetChange.new(id: 0, action: "add")
|
148
|
+
end
|
149
|
+
|
150
|
+
it "sets field to list of ids from scoped_changes" do
|
151
|
+
instance.file = set
|
152
|
+
expect(instance.file_ids).to eq([1, 2])
|
153
|
+
end
|
154
|
+
|
155
|
+
it "leaves any ids that are not touched by the set" do
|
156
|
+
instance.file_ids = [3]
|
157
|
+
instance.file = set
|
158
|
+
expect(instance.file_ids).to include(1, 2, 3)
|
159
|
+
end
|
160
|
+
|
161
|
+
it "removes any ids from field that are scoped as removals" do
|
162
|
+
instance.file_ids = [3]
|
163
|
+
set.remove(3)
|
164
|
+
instance.file = set
|
165
|
+
expect(instance.file_ids).to eq([1, 2])
|
166
|
+
end
|
167
|
+
|
168
|
+
it "adds the AssetChangeSet to pending queue for this field" do
|
169
|
+
instance.file = set
|
170
|
+
new_set = instance.pending_tessa_change_sets[:file]
|
171
|
+
expect(new_set.changes).to eq(set.changes)
|
172
|
+
expect(new_set.scoped_ids).to eq(set.scoped_ids)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
context "with a singular typed field" do
|
178
|
+
before do
|
179
|
+
model.send(:attr_accessor, :file_id)
|
180
|
+
model.asset :file
|
181
|
+
end
|
182
|
+
|
183
|
+
context "when set with an Asset" do
|
184
|
+
let(:asset) { Tessa::Asset.new(id: 1) }
|
185
|
+
|
186
|
+
it "sets field to id from asset" do
|
187
|
+
instance.file = asset
|
188
|
+
expect(instance.file_id).to eq(1)
|
189
|
+
end
|
190
|
+
|
191
|
+
it "adds an AssetChangeSet to pending queue for this field" do
|
192
|
+
instance.file = asset
|
193
|
+
expect(instance.pending_tessa_change_sets[:file]).to be_a(Tessa::AssetChangeSet)
|
194
|
+
end
|
195
|
+
|
196
|
+
describe "the added change set" do
|
197
|
+
let(:set) { instance.pending_tessa_change_sets[:file] }
|
198
|
+
|
199
|
+
it "has an 'add' action for the new asset" do
|
200
|
+
instance.file = asset
|
201
|
+
expect(set.changes.select(&:add?).first.id).to eq(1)
|
202
|
+
end
|
203
|
+
|
204
|
+
it "has a 'remove' action for the previous asset" do
|
205
|
+
instance.file_id = 2
|
206
|
+
instance.file = asset
|
207
|
+
expect(set.changes.select(&:remove?).first.id).to eq(2)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
context "when set with an AssetChangeSet" do
|
213
|
+
let(:set) { Tessa::AssetChangeSet.new }
|
214
|
+
before do
|
215
|
+
set.changes << Tessa::AssetChange.new(id: 0, action: "add")
|
216
|
+
set.add(1)
|
217
|
+
end
|
218
|
+
|
219
|
+
it "sets field to asset id of first 'add' action in scoped_changes" do
|
220
|
+
instance.file = set
|
221
|
+
expect(instance.file_id).to eq(1)
|
222
|
+
end
|
223
|
+
|
224
|
+
it "adds the AssetChangeSet to pending queue for this field" do
|
225
|
+
instance.file = set
|
226
|
+
new_set = instance.pending_tessa_change_sets[:file]
|
227
|
+
expect(new_set.changes).to eq(set.changes)
|
228
|
+
expect(new_set.scoped_ids).to eq(set.scoped_ids)
|
229
|
+
end
|
230
|
+
|
231
|
+
context "with no remove in change set" do
|
232
|
+
it "ensures there is a 'remove' action for previous value" do
|
233
|
+
instance.file_id = 2
|
234
|
+
instance.file = set
|
235
|
+
expect(set.scoped_changes.select(&:remove?).map(&:id)).to eq([2])
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
describe "#asset_getter" do
|
243
|
+
let(:instance) { model.new }
|
244
|
+
subject(:getter) { instance.file }
|
245
|
+
|
246
|
+
context "with a multiple typed field" do
|
247
|
+
before do
|
248
|
+
model.send(:attr_accessor, :file_ids)
|
249
|
+
model.asset :file, multiple: true
|
250
|
+
end
|
251
|
+
|
252
|
+
it "calls find for each of the file_ids and returns result" do
|
253
|
+
instance.file_ids = [1, 2, 3]
|
254
|
+
expect(Tessa::Asset).to receive(:find).with([1, 2, 3]).and_return([:a1, :a2, :a3])
|
255
|
+
expect(getter).to eq([:a1, :a2, :a3])
|
256
|
+
end
|
257
|
+
|
258
|
+
it "caches the result" do
|
259
|
+
expect(Tessa::Asset).to receive(:find).and_return(:val).once
|
260
|
+
instance.file
|
261
|
+
instance.file
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
context "with a singular typed field" do
|
266
|
+
before do
|
267
|
+
model.send(:attr_accessor, :file_id)
|
268
|
+
model.asset :file
|
269
|
+
end
|
270
|
+
|
271
|
+
it "calls find for file_id and returns result" do
|
272
|
+
instance.file_id = 1
|
273
|
+
expect(Tessa::Asset).to receive(:find).with(1).and_return(:a1)
|
274
|
+
expect(getter).to eq(:a1)
|
275
|
+
end
|
276
|
+
|
277
|
+
it "caches the result" do
|
278
|
+
expect(Tessa::Asset).to receive(:find).and_return(:val).once
|
279
|
+
instance.file
|
280
|
+
instance.file
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
describe "#apply_tessa_change_sets" do
|
286
|
+
let(:instance) { model.new }
|
287
|
+
let(:sets) { Array.new(2) { instance_spy(Tessa::AssetChangeSet) } }
|
288
|
+
|
289
|
+
before do
|
290
|
+
model.asset :field1
|
291
|
+
model.asset :field2
|
292
|
+
instance.instance_variable_set(
|
293
|
+
:@pending_tessa_change_sets,
|
294
|
+
{
|
295
|
+
field1: sets[0],
|
296
|
+
field2: sets[1],
|
297
|
+
}
|
298
|
+
)
|
299
|
+
instance.apply_tessa_change_sets
|
300
|
+
end
|
301
|
+
|
302
|
+
it "iterates over all pending changesets calling apply" do
|
303
|
+
expect(sets[0]).to have_received(:apply)
|
304
|
+
expect(sets[1]).to have_received(:apply)
|
305
|
+
end
|
306
|
+
|
307
|
+
it "removes all changesets from list" do
|
308
|
+
expect(instance.pending_tessa_change_sets).to be_empty
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
describe "#remove_all_tessa_assets" do
|
313
|
+
let(:instance) { model.new }
|
314
|
+
before do
|
315
|
+
model.send :attr_accessor, :field1_id, :field2_ids
|
316
|
+
model.asset :field1
|
317
|
+
model.asset :field2, multiple: true
|
318
|
+
instance.field1_id = 1
|
319
|
+
instance.field2_ids = [2, 3]
|
320
|
+
end
|
321
|
+
|
322
|
+
it "adds pending change sets for each field removing all current assets" do
|
323
|
+
instance.remove_all_tessa_assets
|
324
|
+
changes = instance.pending_tessa_change_sets.values
|
325
|
+
.reduce(Tessa::AssetChangeSet.new, :+)
|
326
|
+
.changes
|
327
|
+
.map { |change| [change.id, change.action.to_sym] }
|
328
|
+
expect(changes).to eq([
|
329
|
+
[1, :remove],
|
330
|
+
[2, :remove],
|
331
|
+
[3, :remove],
|
332
|
+
])
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
describe "adds callbacks" do
|
337
|
+
context "model responds to after_commit" do
|
338
|
+
let(:model) {
|
339
|
+
Class.new do
|
340
|
+
def self.after_commit(arg=nil)
|
341
|
+
@after_commit ||= arg
|
342
|
+
end
|
343
|
+
end.tap { |c| c.send(:include, described_module) }
|
344
|
+
}
|
345
|
+
|
346
|
+
it "calls it with :apply_tessa_change_sets" do
|
347
|
+
expect(model.after_commit).to eq(:apply_tessa_change_sets)
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
context "model responds to before_destroy" do
|
352
|
+
let(:model) {
|
353
|
+
Class.new do
|
354
|
+
def self.before_destroy(arg=nil)
|
355
|
+
@before_destroy ||= arg
|
356
|
+
end
|
357
|
+
end.tap { |c| c.send(:include, described_module) }
|
358
|
+
}
|
359
|
+
|
360
|
+
it "calls it with :remove_all_tessa_assets" do
|
361
|
+
expect(model.before_destroy).to eq(:remove_all_tessa_assets)
|
362
|
+
end
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tessa
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Justin Powell
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2015-03-
|
12
|
+
date: 2015-03-19 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: faraday
|
@@ -113,14 +113,22 @@ files:
|
|
113
113
|
- Rakefile
|
114
114
|
- lib/tessa.rb
|
115
115
|
- lib/tessa/asset.rb
|
116
|
+
- lib/tessa/asset_change.rb
|
117
|
+
- lib/tessa/asset_change_set.rb
|
116
118
|
- lib/tessa/config.rb
|
119
|
+
- lib/tessa/model.rb
|
120
|
+
- lib/tessa/model/field.rb
|
117
121
|
- lib/tessa/response_factory.rb
|
118
122
|
- lib/tessa/upload.rb
|
119
123
|
- lib/tessa/version.rb
|
120
124
|
- spec/spec_helper.rb
|
121
125
|
- spec/support/remote_call_macro.rb
|
126
|
+
- spec/tessa/asset_change_set_spec.rb
|
127
|
+
- spec/tessa/asset_change_spec.rb
|
122
128
|
- spec/tessa/asset_spec.rb
|
123
129
|
- spec/tessa/config_spec.rb
|
130
|
+
- spec/tessa/model_field_spec.rb
|
131
|
+
- spec/tessa/model_spec.rb
|
124
132
|
- spec/tessa/upload_spec.rb
|
125
133
|
- tessa.gemspec
|
126
134
|
homepage: https://github.com/watermarkchurch/tessa
|
@@ -150,6 +158,10 @@ summary: Manage your assets.
|
|
150
158
|
test_files:
|
151
159
|
- spec/spec_helper.rb
|
152
160
|
- spec/support/remote_call_macro.rb
|
161
|
+
- spec/tessa/asset_change_set_spec.rb
|
162
|
+
- spec/tessa/asset_change_spec.rb
|
153
163
|
- spec/tessa/asset_spec.rb
|
154
164
|
- spec/tessa/config_spec.rb
|
165
|
+
- spec/tessa/model_field_spec.rb
|
166
|
+
- spec/tessa/model_spec.rb
|
155
167
|
- spec/tessa/upload_spec.rb
|