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