active_remote 1.7.1 → 1.8.0.rc1

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: 4e0a0b815dc24d0ba8c884b135055cc9fb42fbc0
4
- data.tar.gz: 59144b3bb85ecebf7e660e0b5a93a27db60f7751
3
+ metadata.gz: f15daf36d3c8b8835e43b8fdbb36558e9272bf62
4
+ data.tar.gz: ac509c0f6b2a10444c9b7fe1c8fc6a29dec7350e
5
5
  SHA512:
6
- metadata.gz: ac81f4a6f6f7206306e2712a0fb5fe6d90f72228ce63667f480f609d32243e3bee20ad3465cd2766893994ee01c34733a411c9ee5e047cb89781de47d4262d78
7
- data.tar.gz: 471a29c7b76006c8d8fd86835196a3ad5dbbbe76c419c9e5cca6d7f1b86e195fb9b2b149185348fa34df1e8875aba8260a9821838e689ced9d75c9b2b0cd3062
6
+ metadata.gz: 30d4f0bef9362330f7a52adf478766d4a4c9a24c4a9b1e92c359d12f4824444b4d4a55b9a78e85fbfc70f448086dd366467dba82952d99728e552c1635e7d13b
7
+ data.tar.gz: 3f65730eb838053732644492642d4bd32d69d82cfe1be1452b75be61c08ca8f95212832364844d1b31c9b3acc885f7d09c2fcbfb0aeb5f4be873ef9bd01b767f
@@ -39,9 +39,12 @@ module ActiveRemote
39
39
  #
40
40
  def belongs_to(belongs_to_klass, options={})
41
41
  perform_association(belongs_to_klass, options) do |klass, object|
42
- foreign_key = options.fetch(:foreign_key) { :"#{belongs_to_klass}_guid" }
43
- association_guid = object.read_attribute(foreign_key)
44
- klass.search(:guid => association_guid).first if association_guid
42
+ foreign_key = options.fetch(:foreign_key) { :"#{klass.name.demodulize.underscore}_guid" }
43
+ search_hash = {}
44
+ search_hash[:guid] = object.read_attribute(foreign_key)
45
+ search_hash[options[:scope]] = object.read_attribute(options[:scope]) if options.has_key?(:scope)
46
+
47
+ search_hash.values.any?(&:nil?) ? nil : klass.search(search_hash).first
45
48
  end
46
49
  end
47
50
 
@@ -76,9 +79,13 @@ module ActiveRemote
76
79
  # end
77
80
  #
78
81
  def has_many(has_many_class, options={})
79
- perform_association( has_many_class, options ) do |klass, object|
82
+ perform_association(has_many_class, options) do |klass, object|
80
83
  foreign_key = options.fetch(:foreign_key) { :"#{object.class.name.demodulize.underscore}_guid" }
81
- object.guid ? klass.search(foreign_key => object.guid) : []
84
+ search_hash = {}
85
+ search_hash[foreign_key] = object.guid
86
+ search_hash[options[:scope]] = object.read_attribute(options[:scope]) if options.has_key?(:scope)
87
+
88
+ search_hash.values.any?(&:nil?) ? [] : klass.search(search_hash)
82
89
  end
83
90
  end
84
91
 
@@ -99,8 +106,6 @@ module ActiveRemote
99
106
  #
100
107
  # class User
101
108
  # has_one :client
102
- # end
103
- #
104
109
  # An equivalent code snippet without a `has_one` declaration would be:
105
110
  #
106
111
  # ====Examples
@@ -114,19 +119,34 @@ module ActiveRemote
114
119
  def has_one(has_one_klass, options={})
115
120
  perform_association(has_one_klass, options) do |klass, object|
116
121
  foreign_key = options.fetch(:foreign_key) { :"#{object.class.name.demodulize.underscore}_guid" }
117
- klass.search(foreign_key => object.guid).first if object.guid
122
+ search_hash = {}
123
+ search_hash[foreign_key] = object.guid
124
+ search_hash[options[:scope]] = object.read_attribute(options[:scope]) if options.has_key?(:scope)
125
+
126
+ search_hash.values.any?(&:nil?) ? nil : klass.search(search_hash).first
118
127
  end
119
128
  end
120
129
 
130
+ # when requiring an attribute on your search, we verify the attribute
131
+ # exists on both models
132
+ def validate_scoped_attributes(associated_class, object_class, options)
133
+ raise "Could not find attribute: '#{options[:scope]}' on #{object_class}" unless object_class.public_instance_methods.include?(options[:scope])
134
+ raise "Could not find attribute: '#{options[:scope]}' on #{associated_class}" unless associated_class.public_instance_methods.include?(options[:scope])
135
+ end
136
+
121
137
  private
122
138
 
123
- def perform_association(associated_klass, optionz={})
139
+ def perform_association(associated_klass, options={})
140
+
124
141
  define_method(associated_klass) do
142
+ klass_name = options.fetch(:class_name){ associated_klass }
143
+ klass = klass_name.to_s.classify.constantize
144
+
145
+ self.class.validate_scoped_attributes(klass, self.class, options) if options.has_key?(:scope)
146
+
125
147
  value = instance_variable_get(:"@#{associated_klass}")
126
148
 
127
149
  unless value
128
- klass_name = optionz.fetch(:class_name){ associated_klass }
129
- klass = klass_name.to_s.classify.constantize
130
150
  value = yield( klass, self )
131
151
  instance_variable_set(:"@#{associated_klass}", value)
132
152
  end
@@ -134,6 +154,7 @@ module ActiveRemote
134
154
  return value
135
155
  end
136
156
  end
157
+
137
158
  end
138
159
  end
139
160
  end
@@ -1,3 +1,3 @@
1
1
  module ActiveRemote
2
- VERSION = "1.7.1"
2
+ VERSION = "1.8.0.rc1"
3
3
  end
@@ -7,8 +7,9 @@ describe ActiveRemote::Association do
7
7
  describe ".belongs_to" do
8
8
  context "simple association" do
9
9
  let(:author_guid) { "AUT-123" }
10
-
11
- subject { Post.new(:author_guid => author_guid) }
10
+ let(:user_guid) { "USR-123" }
11
+ let(:default_category_guid) { "CAT-123" }
12
+ subject { Post.new(:author_guid => author_guid, :user_guid => user_guid) }
12
13
 
13
14
  it { should respond_to(:author) }
14
15
 
@@ -36,6 +37,32 @@ describe ActiveRemote::Association do
36
37
  subject.author.should be_nil
37
38
  end
38
39
  end
40
+
41
+ context 'scoped field' do
42
+ it { should respond_to(:user) }
43
+
44
+ it "searches the associated model for multiple records" do
45
+ Author.should_receive(:search).with(:guid => subject.author_guid, :user_guid => subject.user_guid).and_return(records)
46
+ subject.user.should eq(record)
47
+ end
48
+
49
+ context 'when user_guid doesnt exist on model 'do
50
+ before { subject.stub(:respond_to?).with("user_guid").and_return(false) }
51
+
52
+ it 'raises an error' do
53
+ expect {subject.user}.to raise_error
54
+ end
55
+ end
56
+
57
+ context 'when user_guid doesnt exist on associated model 'do
58
+ before { Author.stub_chain(:public_instance_methods, :include?).with(:user_guid).and_return(false) }
59
+
60
+ it 'raises an error' do
61
+ expect {subject.user}.to raise_error
62
+ end
63
+ end
64
+ end
65
+
39
66
  end
40
67
 
41
68
  context "specific association with class name" do
@@ -46,19 +73,19 @@ describe ActiveRemote::Association do
46
73
 
47
74
  it "searches the associated model for a single record" do
48
75
  Author.should_receive(:search).with(:guid => subject.author_guid).and_return(records)
49
- subject.author.should eq record
76
+ subject.coauthor.should eq record
50
77
  end
51
78
  end
52
79
 
53
80
  context "specific association with class name and foreign_key" do
54
81
  let(:author_guid) { "AUT-456" }
55
82
 
56
- subject { Post.new(:author_guid => author_guid) }
83
+ subject { Post.new(:bestseller_guid => author_guid) }
57
84
  it { should respond_to(:bestseller) }
58
85
 
59
86
  it "searches the associated model for a single record" do
60
87
  Author.should_receive(:search).with(:guid => subject.bestseller_guid).and_return(records)
61
- subject.author.should eq record
88
+ subject.bestseller.should eq record
62
89
  end
63
90
  end
64
91
  end
@@ -66,8 +93,9 @@ describe ActiveRemote::Association do
66
93
  describe ".has_many" do
67
94
  let(:records) { [ record, record, record ] }
68
95
  let(:guid) { "AUT-123" }
96
+ let(:user_guid) { "USR-123" }
69
97
 
70
- subject { Author.new(:guid => guid) }
98
+ subject { Author.new(:guid => guid, :user_guid => user_guid) }
71
99
 
72
100
  it { should respond_to(:posts) }
73
101
 
@@ -113,55 +141,112 @@ describe ActiveRemote::Association do
113
141
  subject.bestseller_posts.should eq(records)
114
142
  end
115
143
  end
144
+
145
+ context 'scoped field' do
146
+ it { should respond_to(:user_posts) }
147
+
148
+ it "searches the associated model for multiple records" do
149
+ Post.should_receive(:search).with(:author_guid => subject.guid, :user_guid => subject.user_guid).and_return(records)
150
+ subject.user_posts.should eq(records)
151
+ end
152
+
153
+ context 'when user_guid doesnt exist on model 'do
154
+ before { subject.stub(:respond_to?).with("user_guid").and_return(false) }
155
+
156
+ it 'raises an error' do
157
+ expect {subject.user_posts}.to raise_error
158
+ end
159
+ end
160
+
161
+ context 'when user_guid doesnt exist on associated model 'do
162
+ before { Post.stub_chain(:public_instance_methods, :include?).with(:user_guid).and_return(false) }
163
+
164
+ it 'raises an error' do
165
+ expect {subject.user_posts}.to raise_error
166
+ end
167
+ end
168
+ end
116
169
  end
117
170
 
118
171
  describe ".has_one" do
119
- let(:guid) { "PST-123" }
172
+ let(:guid) { "CAT-123" }
173
+ let(:user_guid) { "USR-123" }
174
+ let(:category_attributes) {
175
+ {
176
+ :guid => guid,
177
+ :user_guid => user_guid
178
+ }
179
+ }
120
180
 
121
- subject { Post.new(:guid => guid) }
181
+ subject { Category.new(category_attributes) }
122
182
 
123
- it { should respond_to(:category) }
183
+ it { should respond_to(:author) }
124
184
 
125
185
  it "searches the associated model for all associated records" do
126
- Category.should_receive(:search).with(:post_guid => subject.guid).and_return(records)
127
- subject.category.should eq record
186
+ Author.should_receive(:search).with(:category_guid => subject.guid).and_return(records)
187
+ subject.author.should eq record
128
188
  end
129
189
 
130
190
  it "memoizes the result record" do
131
- Category.should_receive(:search).once.with(:post_guid => subject.guid).and_return(records)
132
- 3.times { subject.category.should eq record }
191
+ Author.should_receive(:search).once.with(:category_guid => subject.guid).and_return(records)
192
+ 3.times { subject.author.should eq record }
133
193
  end
134
194
 
135
195
  context "when guid is nil" do
136
- subject { Post.new }
196
+ subject { Category.new }
137
197
 
138
198
  it "returns nil" do
139
- subject.category.should be_nil
199
+ subject.author.should be_nil
140
200
  end
141
201
  end
142
202
 
143
203
  context "when the search is empty" do
144
204
  it "returns a nil value" do
145
- Category.should_receive(:search).with(:post_guid => subject.guid).and_return([])
146
- subject.category.should be_nil
205
+ Author.should_receive(:search).with(:category_guid => subject.guid).and_return([])
206
+ subject.author.should be_nil
147
207
  end
148
208
  end
149
209
 
150
210
  context "specific association with class name" do
151
- it { should respond_to(:main_category) }
211
+ it { should respond_to(:senior_author) }
152
212
 
153
213
  it "searches the associated model for a single record" do
154
- Category.should_receive(:search).with(:post_guid => subject.guid).and_return(records)
155
- subject.main_category.should eq record
214
+ Author.should_receive(:search).with(:category_guid => subject.guid).and_return(records)
215
+ subject.senior_author.should eq record
156
216
  end
157
217
  end
158
218
 
159
219
  context "specific association with class name and foreign_key" do
160
- it { should respond_to(:default_category) }
220
+ it { should respond_to(:primary_editor) }
161
221
 
162
222
  it "searches the associated model for a single record" do
163
- Category.should_receive(:search).with(:template_post_guid => subject.guid).and_return(records)
164
- subject.default_category.should eq record
223
+ Author.should_receive(:search).with(:editor_guid => subject.guid).and_return(records)
224
+ subject.primary_editor.should eq record
225
+ end
226
+ end
227
+
228
+ context 'scoped field' do
229
+ it { should respond_to(:chief_editor) }
230
+
231
+ it "searches the associated model for multiple records" do
232
+ Author.should_receive(:search).with(:chief_editor_guid => subject.guid, :user_guid => subject.user_guid).and_return(records)
233
+ subject.chief_editor.should eq(record)
234
+ end
235
+
236
+ context 'when user_guid doesnt exist on model 'do
237
+ before { subject.stub(:respond_to?).with("user_guid").and_return(false) }
238
+
239
+ it 'raises an error' do
240
+ expect {subject.chief_editor}.to raise_error
241
+ end
242
+ end
243
+
244
+ context 'when user_guid doesnt exist on associated model 'do
245
+ before { Author.stub_chain(:public_instance_methods, :include?).with(:user_guid).and_return(false) }
246
+
247
+ it 'raises an error' do
248
+ expect {subject.chief_editor}.to raise_error
249
+ end
165
250
  end
166
251
  end
167
252
  end
@@ -6,6 +6,7 @@ message Author {
6
6
  optional string guid = 1;
7
7
  optional string name = 2;
8
8
  repeated Error errors = 3;
9
+ optional string user_guid = 4;
9
10
  }
10
11
 
11
12
  message Authors {
@@ -3,12 +3,13 @@ package generic.remote;
3
3
  import "support/protobuf/error.proto";
4
4
  import "support/protobuf/category.proto";
5
5
 
6
- message Post {
6
+ message Post {
7
7
  optional string guid = 1;
8
8
  optional string name = 2;
9
9
  optional string author_guid = 3;
10
10
  optional Category category = 4;
11
11
  repeated Error errors = 5;
12
+ optional string user_guid = 6;
12
13
  }
13
14
 
14
15
  message Posts {
@@ -19,6 +20,7 @@ message PostRequest {
19
20
  repeated string guid = 1;
20
21
  repeated string name = 2;
21
22
  repeated string author_guid = 3;
23
+ repeated string user_guid = 4;
22
24
  }
23
25
 
24
26
  service PostService {
@@ -2,10 +2,14 @@ package generic.remote;
2
2
 
3
3
  import "support/protobuf/error.proto";
4
4
 
5
- message Category {
5
+ message Category {
6
6
  optional string guid = 1;
7
7
  optional string name = 2;
8
8
  repeated Error errors = 3;
9
+ optional string user_guid = 4;
10
+ optional string author_guid = 4;
11
+ optional string chief_editor_guid = 4;
12
+ optional string editor_guid = 4;
9
13
  }
10
14
 
11
15
  message Categories {
@@ -8,9 +8,15 @@ class Author < ::ActiveRemote::Base
8
8
 
9
9
  attribute :guid
10
10
  attribute :name
11
+ attribute :user_guid
12
+ attribute :chief_editor_guid
13
+ attribute :editor_guid
14
+ attribute :category_guid
11
15
 
12
16
  has_many :posts
17
+ has_many :user_posts, :class_name => "::Post", :scope => :user_guid
13
18
  has_many :flagged_posts, :class_name => "::Post"
14
19
  has_many :bestseller_posts, :class_name => "::Post", :foreign_key => :bestseller_guid
15
20
 
21
+ belongs_to :category
16
22
  end
@@ -7,10 +7,14 @@ class Category < ::ActiveRemote::Base
7
7
  service_class ::Generic::Remote::CategoryService
8
8
 
9
9
  attribute :guid
10
- attribute :name
11
- attribute :post_id
10
+ attribute :user_guid
11
+ attribute :chief_editor_guid
12
12
 
13
- belongs_to :post
13
+ has_many :posts
14
+
15
+ has_one :author
16
+ has_one :senior_author, :class_name => "::Author"
17
+ has_one :primary_editor, :class_name => "::Author", :foreign_key => :editor_guid
18
+ has_one :chief_editor, :class_name => "::Author", :scope => :user_guid, :foreign_key => :chief_editor_guid
14
19
 
15
- alias_method :template_post_guid, :post_id
16
20
  end
@@ -9,13 +9,11 @@ class Post < ::ActiveRemote::Base
9
9
  attribute :guid
10
10
  attribute :name
11
11
  attribute :author_guid
12
+ attribute :user_guid
13
+ attribute :bestseller_guid
12
14
 
13
15
  belongs_to :author
14
16
  belongs_to :coauthor, :class_name => '::Author'
15
17
  belongs_to :bestseller, :class_name => '::Author', :foreign_key => :bestseller_guid
16
- has_one :category
17
- has_one :main_category, :class_name => '::Category'
18
- has_one :default_category, :class_name => '::Category', :foreign_key => :template_post_guid
19
-
20
- alias_method :bestseller_guid, :author_guid
18
+ belongs_to :user, :class_name => '::Author', :scope => :user_guid
21
19
  end
@@ -4,6 +4,7 @@
4
4
  require 'protobuf/message'
5
5
  require 'protobuf/rpc/service'
6
6
 
7
+
7
8
  ##
8
9
  # Imports
9
10
  #
@@ -11,14 +12,15 @@ require 'support/protobuf/error.pb'
11
12
 
12
13
  module Generic
13
14
  module Remote
14
-
15
+
15
16
  ##
16
17
  # Message Classes
17
18
  #
18
19
  class Author < ::Protobuf::Message; end
19
20
  class Authors < ::Protobuf::Message; end
20
21
  class AuthorRequest < ::Protobuf::Message; end
21
-
22
+
23
+
22
24
  ##
23
25
  # Message Fields
24
26
  #
@@ -26,19 +28,21 @@ module Generic
26
28
  optional ::Protobuf::Field::StringField, :guid, 1
27
29
  optional ::Protobuf::Field::StringField, :name, 2
28
30
  repeated ::Generic::Error, :errors, 3
31
+ optional ::Protobuf::Field::StringField, :user_guid, 4
29
32
  end
30
-
33
+
31
34
  class Authors
32
35
  repeated ::Generic::Remote::Author, :records, 1
33
36
  end
34
-
37
+
35
38
  class AuthorRequest
36
39
  repeated ::Protobuf::Field::StringField, :guid, 1
37
40
  repeated ::Protobuf::Field::StringField, :name, 2
38
41
  end
39
-
42
+
43
+
40
44
  ##
41
- # Services
45
+ # Service Classes
42
46
  #
43
47
  class AuthorService < ::Protobuf::Rpc::Service
44
48
  rpc :search, ::Generic::Remote::AuthorRequest, ::Generic::Remote::Authors
@@ -50,5 +54,8 @@ module Generic
50
54
  rpc :delete_all, ::Generic::Remote::Authors, ::Generic::Remote::Authors
51
55
  rpc :destroy_all, ::Generic::Remote::Authors, ::Generic::Remote::Authors
52
56
  end
57
+
53
58
  end
59
+
54
60
  end
61
+
@@ -4,6 +4,7 @@
4
4
  require 'protobuf/message'
5
5
  require 'protobuf/rpc/service'
6
6
 
7
+
7
8
  ##
8
9
  # Imports
9
10
  #
@@ -12,14 +13,15 @@ require 'support/protobuf/category.pb'
12
13
 
13
14
  module Generic
14
15
  module Remote
15
-
16
+
16
17
  ##
17
18
  # Message Classes
18
19
  #
19
20
  class Post < ::Protobuf::Message; end
20
21
  class Posts < ::Protobuf::Message; end
21
22
  class PostRequest < ::Protobuf::Message; end
22
-
23
+
24
+
23
25
  ##
24
26
  # Message Fields
25
27
  #
@@ -29,20 +31,23 @@ module Generic
29
31
  optional ::Protobuf::Field::StringField, :author_guid, 3
30
32
  optional ::Generic::Remote::Category, :category, 4
31
33
  repeated ::Generic::Error, :errors, 5
34
+ optional ::Protobuf::Field::StringField, :user_guid, 6
32
35
  end
33
-
36
+
34
37
  class Posts
35
38
  repeated ::Generic::Remote::Post, :records, 1
36
39
  end
37
-
40
+
38
41
  class PostRequest
39
42
  repeated ::Protobuf::Field::StringField, :guid, 1
40
43
  repeated ::Protobuf::Field::StringField, :name, 2
41
44
  repeated ::Protobuf::Field::StringField, :author_guid, 3
45
+ repeated ::Protobuf::Field::StringField, :user_guid, 4
42
46
  end
43
-
47
+
48
+
44
49
  ##
45
- # Services
50
+ # Service Classes
46
51
  #
47
52
  class PostService < ::Protobuf::Rpc::Service
48
53
  rpc :search, ::Generic::Remote::PostRequest, ::Generic::Remote::Posts
@@ -54,5 +59,8 @@ module Generic
54
59
  rpc :delete_all, ::Generic::Remote::Posts, ::Generic::Remote::Posts
55
60
  rpc :destroy_all, ::Generic::Remote::Posts, ::Generic::Remote::Posts
56
61
  end
62
+
57
63
  end
64
+
58
65
  end
66
+
@@ -4,6 +4,7 @@
4
4
  require 'protobuf/message'
5
5
  require 'protobuf/rpc/service'
6
6
 
7
+
7
8
  ##
8
9
  # Imports
9
10
  #
@@ -11,14 +12,15 @@ require 'support/protobuf/error.pb'
11
12
 
12
13
  module Generic
13
14
  module Remote
14
-
15
+
15
16
  ##
16
17
  # Message Classes
17
18
  #
18
19
  class Tag < ::Protobuf::Message; end
19
20
  class Tags < ::Protobuf::Message; end
20
21
  class TagRequest < ::Protobuf::Message; end
21
-
22
+
23
+
22
24
  ##
23
25
  # Message Fields
24
26
  #
@@ -27,18 +29,19 @@ module Generic
27
29
  optional ::Protobuf::Field::StringField, :name, 2
28
30
  repeated ::Generic::Error, :errors, 3
29
31
  end
30
-
32
+
31
33
  class Tags
32
34
  repeated ::Generic::Remote::Tag, :records, 1
33
35
  end
34
-
36
+
35
37
  class TagRequest
36
38
  repeated ::Protobuf::Field::StringField, :guid, 1
37
39
  repeated ::Protobuf::Field::StringField, :name, 2
38
40
  end
39
-
41
+
42
+
40
43
  ##
41
- # Services
44
+ # Service Classes
42
45
  #
43
46
  class TagService < ::Protobuf::Rpc::Service
44
47
  rpc :search, ::Generic::Remote::TagRequest, ::Generic::Remote::Tags
@@ -50,5 +53,8 @@ module Generic
50
53
  rpc :delete_all, ::Generic::Remote::Tags, ::Generic::Remote::Tags
51
54
  rpc :destroy_all, ::Generic::Remote::Tags, ::Generic::Remote::Tags
52
55
  end
56
+
53
57
  end
58
+
54
59
  end
60
+
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_remote
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.1
4
+ version: 1.8.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adam Hutchison
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-11-26 00:00:00.000000000 Z
11
+ date: 2014-01-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: active_attr
@@ -235,12 +235,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
235
235
  version: '0'
236
236
  required_rubygems_version: !ruby/object:Gem::Requirement
237
237
  requirements:
238
- - - '>='
238
+ - - '>'
239
239
  - !ruby/object:Gem::Version
240
- version: '0'
240
+ version: 1.3.1
241
241
  requirements: []
242
242
  rubyforge_project:
243
- rubygems_version: 2.1.11
243
+ rubygems_version: 2.2.1
244
244
  signing_key:
245
245
  specification_version: 4
246
246
  summary: Active Record for your platform