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