tessa 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 255c6add5ff6306866a4a1dee9824a0accc98798
4
- data.tar.gz: e501ec81adef345982f8b2c0265d151b032b0048
3
+ metadata.gz: a4842f3250777ed9f020f3096a04782f1e740a7d
4
+ data.tar.gz: a1984882ebc543a5a0acf098f54b4c081f61b8b0
5
5
  SHA512:
6
- metadata.gz: e05ba5fb9523cbf9470a3bc162f98972d4d0b9affc29b8f90127ebc188c107e63e3dec1b558a3c7a295e3adfd03a3db16a0c2eb8e98b32075c40eb70b5759f76
7
- data.tar.gz: 9c4f7b70485c409485d1e8750c3321cd2d7be724a8f8f6487e251ed733872caec0ce1176ce996db38322916f8afc347e73ac43d17313efea87c902ff2a279648
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; end
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
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Tessa
2
- VERSION = "0.1.3"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -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(Tessa::RequestFailed)
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.1.3
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-13 00:00:00.000000000 Z
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