essential 0.9.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 +7 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +35 -0
- data/LICENSE +21 -0
- data/README.md +107 -0
- data/Rakefile +9 -0
- data/essential.gemspec +26 -0
- data/lib/essential.rb +32 -0
- data/lib/essential/account.rb +56 -0
- data/lib/essential/client.rb +65 -0
- data/lib/essential/errors/api_error.rb +54 -0
- data/lib/essential/messaging/channel.rb +36 -0
- data/lib/essential/messaging/message.rb +68 -0
- data/lib/essential/messaging/property.rb +27 -0
- data/lib/essential/messaging/subscriber.rb +64 -0
- data/lib/essential/messaging/transport.rb +29 -0
- data/lib/essential/resource.rb +127 -0
- data/lib/essential/resource/attr_methods.rb +65 -0
- data/lib/essential/resource/attr_relations.rb +81 -0
- data/lib/essential/resource/create.rb +21 -0
- data/lib/essential/resource/delete.rb +20 -0
- data/lib/essential/resource/list.rb +17 -0
- data/lib/essential/resource/paginator_proxy.rb +115 -0
- data/lib/essential/resource/update.rb +26 -0
- data/lib/essential/version.rb +3 -0
- data/test/essential/account_test.rb +22 -0
- data/test/essential/messaging/channel_test.rb +62 -0
- data/test/essential/messaging/message_test.rb +119 -0
- data/test/essential/messaging/property_test.rb +27 -0
- data/test/essential/messaging/subscriber_test.rb +106 -0
- data/test/essential/messaging/transport_test.rb +37 -0
- data/test/integration/alternate_authentication_test.rb +128 -0
- data/test/test_helper.rb +34 -0
- metadata +125 -0
@@ -0,0 +1,68 @@
|
|
1
|
+
module Essential::Messaging
|
2
|
+
class Message < Essential::Resource
|
3
|
+
extend Essential::Resource::Create
|
4
|
+
extend Essential::Resource::List
|
5
|
+
include Essential::Resource::Delete
|
6
|
+
|
7
|
+
attr_property :status, :body, :media_urls,
|
8
|
+
:notified_received_at, :notified_received_result,
|
9
|
+
:onstatus_url, :status_updated_at,
|
10
|
+
:notified_status_at, :notified_status_result,
|
11
|
+
:delivery_attempts, :delivery_attempted_at,
|
12
|
+
:msecs_in_flight,
|
13
|
+
:created_at, :updated_at
|
14
|
+
|
15
|
+
# Timestamps, one and all!
|
16
|
+
attr_schema [
|
17
|
+
:notified_received_at, :status_updated_at, :notified_status_at,
|
18
|
+
:delivery_attempted_at, :created_at, :updated_at
|
19
|
+
].reduce({}) {|h,k| h[k] = Time; h}
|
20
|
+
|
21
|
+
# special aliases for create
|
22
|
+
attr_unfiltered :subscriber, :transport, :channel
|
23
|
+
|
24
|
+
attr_relation account_sid: 'Essential::Account'
|
25
|
+
attr_relation channel_sid: 'Channel'
|
26
|
+
attr_relation transport_sid: 'Transport'
|
27
|
+
attr_relation subscriber_sid: 'Subscriber'
|
28
|
+
|
29
|
+
alias :redact :delete
|
30
|
+
|
31
|
+
def self.status(params: {}, headers: @headers)
|
32
|
+
# permitted:
|
33
|
+
# :start_date, :end_date, :carrier, :status, :channel, :channel_sid
|
34
|
+
resp = self.request(
|
35
|
+
:get,
|
36
|
+
url: '/v2/account/messages/analytics/status',
|
37
|
+
params: params,
|
38
|
+
headers: headers
|
39
|
+
)
|
40
|
+
JSON.parse(resp)
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.queue_status(params: {}, headers: @headers)
|
44
|
+
# permitted:
|
45
|
+
# :channel, :channel_name
|
46
|
+
resp = self.request(
|
47
|
+
:get,
|
48
|
+
url: '/v2/account/messages/analytics/queue_status',
|
49
|
+
params: params,
|
50
|
+
headers: headers
|
51
|
+
)
|
52
|
+
JSON.parse(resp)
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.seconds_in_flight(params: {}, headers: @headers)
|
56
|
+
# permitted:
|
57
|
+
# :start_date, :end_date, :carrier
|
58
|
+
resp = self.request(
|
59
|
+
:get,
|
60
|
+
url: '/v2/account/messages/analytics/seconds_in_flight',
|
61
|
+
params: params,
|
62
|
+
headers: headers
|
63
|
+
)
|
64
|
+
JSON.parse(resp)
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Essential::Messaging
|
2
|
+
class Property < Essential::Resource
|
3
|
+
include Essential::Resource::Update
|
4
|
+
include Essential::Resource::Delete
|
5
|
+
|
6
|
+
undef_method :sid
|
7
|
+
attr_property :subscriber_sid, :name, :value
|
8
|
+
|
9
|
+
def self.retrieve(*args)
|
10
|
+
raise NotImplementedError, 'must be retrieved from a Subscriber'
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.url(subscriber_sid)
|
14
|
+
format('%s/%s/properties', Subscriber.url, CGI.escape(subscriber_sid))
|
15
|
+
end
|
16
|
+
|
17
|
+
def url
|
18
|
+
format('%s/%s', self.class.url(subscriber_sid), CGI.escape(name))
|
19
|
+
end
|
20
|
+
|
21
|
+
protected
|
22
|
+
|
23
|
+
def loaded?
|
24
|
+
@attributes.key?('name') && @attributes.key?('value')
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Essential::Messaging
|
2
|
+
class Subscriber < Essential::Resource
|
3
|
+
extend Essential::Resource::Create
|
4
|
+
extend Essential::Resource::List
|
5
|
+
include Essential::Resource::Update
|
6
|
+
include Essential::Resource::Delete
|
7
|
+
|
8
|
+
attr_property :phone_number, :status, :carrier,
|
9
|
+
:subscribed_at, :unsubscribed_at, :created_at, :updated_at
|
10
|
+
|
11
|
+
# special aliases for create
|
12
|
+
attr_unfiltered :transport, :channel
|
13
|
+
|
14
|
+
attr_schema subscribed_at: Time, unsubscribed_at: Time, created_at: Time, updated_at: Time
|
15
|
+
|
16
|
+
attr_relation account_sid: 'Essential::Account'
|
17
|
+
attr_relation channel_sid: 'Channel'
|
18
|
+
attr_relation transport_sid: 'Transport'
|
19
|
+
|
20
|
+
alias :unsubscribe :delete
|
21
|
+
|
22
|
+
def messages
|
23
|
+
Essential::Resource::PaginatorProxy.new(
|
24
|
+
Message,
|
25
|
+
params: {subscriber: self.sid},
|
26
|
+
headers: @headers
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
def properties
|
31
|
+
Essential::Resource::PaginatorProxy.new(
|
32
|
+
Property,
|
33
|
+
url: Property.url(self.sid),
|
34
|
+
headers: @headers,
|
35
|
+
attrs: {subscriber_sid: self.sid},
|
36
|
+
)
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.by_carrier(params: {}, headers: @headers)
|
40
|
+
# permitted:
|
41
|
+
# :start_date, :end_date, :carrier
|
42
|
+
resp = self.request(
|
43
|
+
:get,
|
44
|
+
url: '/v2/account/subscribers/analytics/by_carrier',
|
45
|
+
params: params,
|
46
|
+
headers: headers
|
47
|
+
)
|
48
|
+
JSON.parse(resp)
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.by_transport(params: {}, headers: @headers)
|
52
|
+
# permitted:
|
53
|
+
# :start_date, :end_date, :carrier
|
54
|
+
resp = self.request(
|
55
|
+
:get,
|
56
|
+
url: '/v2/account/subscribers/analytics/by_transport',
|
57
|
+
params: params,
|
58
|
+
headers: headers
|
59
|
+
)
|
60
|
+
JSON.parse(resp)
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Essential::Messaging
|
2
|
+
class Transport < Essential::Resource
|
3
|
+
extend Essential::Resource::List
|
4
|
+
|
5
|
+
attr_property :protocol, :endpoint, :created_at, :updated_at
|
6
|
+
attr_schema created_at: Time, updated_at: Time
|
7
|
+
|
8
|
+
attr_unfiltered :channel
|
9
|
+
|
10
|
+
attr_relation account_sid: 'Essential::Account'
|
11
|
+
attr_relation channel_sid: 'Channel'
|
12
|
+
|
13
|
+
def subscribers
|
14
|
+
Essential::Resource::PaginatorProxy.new(
|
15
|
+
Subscriber,
|
16
|
+
params: {transport: self.sid},
|
17
|
+
headers: @headers
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
def messages
|
22
|
+
Essential::Resource::PaginatorProxy.new(
|
23
|
+
Message,
|
24
|
+
params: {transport: self.sid},
|
25
|
+
headers: @headers
|
26
|
+
)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
require 'essential/resource/attr_methods'
|
2
|
+
require 'essential/resource/create'
|
3
|
+
require 'essential/resource/list'
|
4
|
+
require 'essential/resource/update'
|
5
|
+
require 'essential/resource/delete'
|
6
|
+
|
7
|
+
module Essential
|
8
|
+
class Resource
|
9
|
+
include AttrMethods
|
10
|
+
|
11
|
+
class << self
|
12
|
+
attr_reader :schema
|
13
|
+
|
14
|
+
def request(method, url: nil, params: nil, headers: nil)
|
15
|
+
if headers
|
16
|
+
headers = headers.clone
|
17
|
+
sid = headers.delete(:sid)
|
18
|
+
token = headers.delete(:token)
|
19
|
+
end
|
20
|
+
|
21
|
+
Essential.request(method, url, sid: sid, token: token, params: params, headers: headers)
|
22
|
+
end
|
23
|
+
|
24
|
+
protected
|
25
|
+
|
26
|
+
def class_name
|
27
|
+
self.name.split('::').last
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
attr_property :sid
|
32
|
+
|
33
|
+
def self.url
|
34
|
+
if self == Resource
|
35
|
+
raise NotImplementedError, 'Resource is an abstract class'
|
36
|
+
end
|
37
|
+
"/v2/account/#{CGI.escape(class_name.downcase)}s"
|
38
|
+
end
|
39
|
+
|
40
|
+
def url
|
41
|
+
raise ArgumentError, 'sid required' if sid.nil? || sid.empty?
|
42
|
+
format('%s/%s', self.class.url, CGI.escape(self.sid))
|
43
|
+
end
|
44
|
+
|
45
|
+
def initialize(sid: nil, attrs: nil, headers: nil)
|
46
|
+
init_from(attrs)
|
47
|
+
@attributes['sid'] = sid if sid
|
48
|
+
@headers = headers
|
49
|
+
self
|
50
|
+
end
|
51
|
+
|
52
|
+
def fetch(headers: @headers)
|
53
|
+
json = JSON.parse self.class.request(:get, url: self.url, headers: headers)
|
54
|
+
init_from json
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
def init_from(attributes)
|
59
|
+
attributes = (attributes || {}).clone
|
60
|
+
|
61
|
+
# reject reject keys that aren't actually our attrs
|
62
|
+
@attributes = filter_attrs(attributes)
|
63
|
+
|
64
|
+
if self.class.schema
|
65
|
+
self.class.schema.each do |key,type|
|
66
|
+
apply_schema(key, type)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
self
|
70
|
+
end
|
71
|
+
|
72
|
+
def loaded?
|
73
|
+
@attributes.size > 1
|
74
|
+
end
|
75
|
+
|
76
|
+
def as_json(opts={})
|
77
|
+
@attributes.dup
|
78
|
+
end
|
79
|
+
|
80
|
+
def to_json(opts={})
|
81
|
+
as_json.to_json(opts)
|
82
|
+
end
|
83
|
+
|
84
|
+
def ==(other)
|
85
|
+
case other
|
86
|
+
when Essential::Resource
|
87
|
+
self.sid == other.sid
|
88
|
+
else
|
89
|
+
false
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def inspect
|
94
|
+
format(
|
95
|
+
'#<%s:0x%s @attributes=%s>',
|
96
|
+
self.class.name,
|
97
|
+
(self.object_id << 1).to_s(16),
|
98
|
+
JSON.pretty_generate(@attributes)
|
99
|
+
)
|
100
|
+
end
|
101
|
+
|
102
|
+
protected
|
103
|
+
def [](key)
|
104
|
+
# realize attributes upon access
|
105
|
+
self.fetch unless :sid == key.to_sym || loaded?
|
106
|
+
@attributes[key]
|
107
|
+
end
|
108
|
+
|
109
|
+
def apply_schema(key, type)
|
110
|
+
if val = @attributes[key]
|
111
|
+
if type === val
|
112
|
+
return
|
113
|
+
elsif type.respond_to?(:parse)
|
114
|
+
val = type.parse(@attributes[key])
|
115
|
+
if type == Time && Essential.utc_offset
|
116
|
+
# coerce into local time
|
117
|
+
val = val.getlocal(Essential.utc_offset)
|
118
|
+
end
|
119
|
+
@attributes[key] = val
|
120
|
+
else
|
121
|
+
raise ArgumentError, format('unsure how to apply schema "%s"', type)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'essential/resource/attr_relations'
|
2
|
+
|
3
|
+
module Essential
|
4
|
+
class Resource
|
5
|
+
module AttrMethods
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
include AttrRelations
|
9
|
+
|
10
|
+
def from_attributes(attributes, headers: @headers)
|
11
|
+
self.new(attrs: attributes, headers: headers)
|
12
|
+
end
|
13
|
+
|
14
|
+
def permitted_attrs
|
15
|
+
_permitted_attrs.dup
|
16
|
+
end
|
17
|
+
|
18
|
+
def filter_attrs(attributes)
|
19
|
+
filtered = {}
|
20
|
+
attributes.keys.each do |k|
|
21
|
+
if self.permitted_attrs.include?(k.to_sym)
|
22
|
+
filtered[k.to_s] = attributes[k]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
filtered
|
27
|
+
end
|
28
|
+
|
29
|
+
protected
|
30
|
+
|
31
|
+
def _permitted_attrs
|
32
|
+
@permitted_attrs ||= Set[:sid]
|
33
|
+
end
|
34
|
+
|
35
|
+
def attr_property(*names)
|
36
|
+
attr_unfiltered(*names)
|
37
|
+
names.each do |name|
|
38
|
+
define_method(name.to_sym) do
|
39
|
+
self[name.to_s]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def attr_unfiltered(*names)
|
45
|
+
_permitted_attrs.merge names.map(&:to_sym)
|
46
|
+
end
|
47
|
+
|
48
|
+
def attr_schema(pairs)
|
49
|
+
@schema ||= {}
|
50
|
+
@schema = @schema.merge(Hash[pairs.map{|k,v| [k.to_s, v]}])
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
def filter_attrs(attributes)
|
56
|
+
self.class.filter_attrs(attributes)
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.included(base)
|
60
|
+
base.extend(ClassMethods)
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Essential
|
2
|
+
class Resource
|
3
|
+
module AttrRelations
|
4
|
+
|
5
|
+
# Method for defining a mapping between a `sid` property and a Class.
|
6
|
+
#
|
7
|
+
def attr_relation(pairs)
|
8
|
+
pairs.each do |method, resource|
|
9
|
+
sid_method = method.to_sym
|
10
|
+
|
11
|
+
# Enforces that we're dealing with a `sid` property.
|
12
|
+
if method =~ /\A(.+)_sid\Z/
|
13
|
+
method = $1.to_sym
|
14
|
+
else
|
15
|
+
raise ArgumentError, 'attribute must end in _sid'
|
16
|
+
end
|
17
|
+
|
18
|
+
unless method_defined?(sid_method)
|
19
|
+
# perhaps we didn't explicitly define access to the sid - do so now.
|
20
|
+
attr_property sid_method
|
21
|
+
end
|
22
|
+
|
23
|
+
# Avoid circular load dependencies:
|
24
|
+
#
|
25
|
+
# `resource` might be a Class, or it might be a String.
|
26
|
+
#
|
27
|
+
# If it is a String, we'll attempt to turn it into a CLass.
|
28
|
+
# This will be done once - the first time it is accessed -
|
29
|
+
# to allow both classes to load completely before constantizing
|
30
|
+
# the String.
|
31
|
+
my_lazy_resource = format('lazy_%s_resource', method).to_sym
|
32
|
+
my_lazy_variable = format('@%s', my_lazy_resource).to_sym
|
33
|
+
define_singleton_method(my_lazy_resource) do
|
34
|
+
# only resolve this resource once
|
35
|
+
lazy_resource = instance_variable_get(my_lazy_variable)
|
36
|
+
unless lazy_resource
|
37
|
+
lazy_resource = resource
|
38
|
+
|
39
|
+
case lazy_resource
|
40
|
+
when String
|
41
|
+
if lazy_resource.include?('::')
|
42
|
+
# this appears to be a fully-namespaced Resource
|
43
|
+
lazy_resource = lazy_resource.split('::')
|
44
|
+
else
|
45
|
+
# no namespace - assume it's part of the current class.
|
46
|
+
namespace = self.name.split('::')
|
47
|
+
namespace.pop
|
48
|
+
namespace << lazy_resource
|
49
|
+
lazy_resource = namespace
|
50
|
+
end
|
51
|
+
# fetch each module in turn
|
52
|
+
lazy_resource = lazy_resource.reduce(Object) {|o,c| o.const_get c}
|
53
|
+
end
|
54
|
+
|
55
|
+
# now that we have a class, ensure it is *actually* a Resource!
|
56
|
+
unless lazy_resource < Essential::Resource
|
57
|
+
raise ArgumentError, format('mapping must be an %s', Essential::Resource.name)
|
58
|
+
end
|
59
|
+
|
60
|
+
# cache it.
|
61
|
+
instance_variable_set(my_lazy_variable, lazy_resource)
|
62
|
+
end
|
63
|
+
|
64
|
+
lazy_resource
|
65
|
+
end
|
66
|
+
|
67
|
+
define_method(method) do
|
68
|
+
sid = send(sid_method)
|
69
|
+
if sid.nil?
|
70
|
+
nil
|
71
|
+
else
|
72
|
+
self.class.send(my_lazy_resource).new(sid: sid, headers: @headers)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|