tms_client 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "https://rubygems.org"
2
+ gemspec
data/README.md ADDED
@@ -0,0 +1,134 @@
1
+ TMS Client
2
+ ===========
3
+ This is a ruby client to interact with the GovDelivery TMS REST API.
4
+
5
+
6
+ Usage
7
+ -----
8
+ ### Connecting
9
+
10
+ ``` ruby
11
+ client = TMS::Client.new('username', 'password', :api_root => 'https://tms.govdelivery.com')
12
+
13
+ ```
14
+
15
+ ### Getting messages
16
+
17
+ ``` ruby
18
+ client.subresources #=> {"messages"=><TMS::Messages href=/messages collection=[]>}
19
+ client.messages #=> <TMS::Messages href=/messages collection=[]>
20
+ client.sms_messages.get #=> #lots of sms stuff
21
+ client.sms_messages.next #=> <TMS::Messages href=/messages/page/2 collection=[]>
22
+ # (if there is a second page)
23
+ client.sms_messages.next.get #=> # more messages...
24
+ client.voice_messages.get #=> #lots of voice stuff
25
+ client.voice_messages.next #=> <TMS::Messages href=/messages/page/2 collection=[]>
26
+ # (if there is a second page)
27
+ client.voice_messages.next.get #=> # more messages...
28
+ ```
29
+
30
+
31
+ ### Sending an SMS Message
32
+
33
+ ``` ruby
34
+ message = client.sms_messages.build(:short_body=>'Test Message!')
35
+ message.recipients.build(:phone=>'5551112222')
36
+ message.recipients.build(:phone=>'5551112223')
37
+ message.recipients.build # invalid - no phone
38
+ message.post #=> true
39
+ message.recipients.collection.detect{|r| r.errors } #=> {"phone"=>["is not a number"]}
40
+ # save succeeded, but we have one bad recipient
41
+ message.href #=> "/messages/87"
42
+ message.get #=> <TMS::Message href=/messages/87 attributes={...}>
43
+ ```
44
+
45
+ ### Sending an Voice Message
46
+
47
+ ``` ruby
48
+ message = client.voice_messages.build(:url=>'www.testmessage.com')
49
+ message.recipients.build(:phone=>'5551112222')
50
+ message.recipients.build(:phone=>'5551112223')
51
+ message.recipients.build # invalid - no phone
52
+ message.post #=> true
53
+ message.recipients.collection.detect{|r| r.errors } #=> {"phone"=>["is not a number"]}
54
+ # save succeeded, but we have one bad recipient
55
+ message.href #=> "/messages/87"
56
+ message.get #=> <TMS::Message href=/messages/87 attributes={...}>
57
+ ```
58
+
59
+ ### Listing Command Types
60
+
61
+ ``` ruby
62
+ command_types = client.command_types.get
63
+ command_types.collection.each do |at|
64
+ puts at.name #=> "forward"
65
+ puts at.fields #=> ["url", "http_method", ...]
66
+ end
67
+ ````
68
+
69
+ ### Managing Keywords
70
+
71
+ ``` ruby
72
+ # CRUD
73
+ keyword = client.keywords.build(:name => "BUSRIDE")
74
+ keyword.post #=> true
75
+ keyword.name #=> 'busride'
76
+ keyword.name = "TRAINRIDE"
77
+ keyword.put #=> true
78
+ keyword.name #=> 'trainride'
79
+ keyword.delete #=> true
80
+
81
+ # list
82
+ keywords = client.keywords.get
83
+ keywords.collection.each do |k|
84
+ puts k.name
85
+ end
86
+ ```
87
+
88
+ ### Managing Commands
89
+
90
+ ```ruby
91
+ # CRUD
92
+ keywords = client.keywords.get
93
+ keyword = keywords.collection.first.get
94
+ command = keyword.commands.build(
95
+ :name => "Forward to somewhere else",
96
+ :params => {:url => "http://example.com", :http_method => "get"},
97
+ :command_type => :forward)
98
+ command.post
99
+ command.params = {:url => "http://example.com/new_url", :http_method => "post"}
100
+ command.put
101
+ command.delete
102
+
103
+ # list
104
+ commands = keyword.commands.get
105
+ commands.collection.each do |c|
106
+ puts c.inspect
107
+ end
108
+ ```
109
+
110
+
111
+ ### Logging
112
+ Any instance of a [Logger](http://www.ruby-doc.org/stdlib-1.9.3/libdoc/logger/rdoc/Logger.html "Ruby Logger")-like class can be passed in to the client; incoming and outgoing
113
+ request information will then be logged to that instance.
114
+
115
+ The example below configures `TMS::Client` to log to STDOUT:
116
+
117
+ ``` ruby
118
+ logger = Logger.new(STDOUT)
119
+ client = TMS::Client.new('username', 'password', :logger => logger)
120
+
121
+ ```
122
+
123
+ License
124
+ -------
125
+ Copyright (c) 2013, GovDelivery, Inc.
126
+
127
+ All rights reserved.
128
+
129
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
130
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
131
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
132
+ * Neither the name of GovDelivery nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
133
+
134
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require 'rspec/core/rake_task'
2
+ require 'rubygems/tasks'
3
+ require 'rubygems/tasks/scm'
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+ Gem::Tasks.new
7
+
8
+ task :default => :spec
@@ -0,0 +1,28 @@
1
+ module TMS #:nodoc:
2
+ module Base
3
+ def self.included(base)
4
+ base.send(:include, TMS::Util::HalLinkParser)
5
+ base.extend(ClassMethods)
6
+ base.send(:include, InstanceMethods)
7
+ base.send(:include, TMS::CoreExt)
8
+ base.send(:extend, TMS::CoreExt)
9
+ end
10
+
11
+ attr_accessor :client, :href, :errors
12
+
13
+ module ClassMethods
14
+ def to_param
15
+ tmsify(self)
16
+ end
17
+ end
18
+
19
+ module InstanceMethods
20
+ def initialize(client, href)
21
+ self.client = client
22
+ self.href = href
23
+ end
24
+ end
25
+
26
+ end
27
+
28
+ end
@@ -0,0 +1,80 @@
1
+ # The client class to connect and talk to the TMS REST API.
2
+ class TMS::Client
3
+ include TMS::Util::HalLinkParser
4
+ include TMS::CoreExt
5
+
6
+ attr_accessor :connection, :href
7
+
8
+ # Create a new client and issue a request for the available resources for a given account.
9
+ #
10
+ # === Options
11
+ # * +:api_root+ - The root URL of the TMS api. Defaults to localhost:3000
12
+ # * +:logger+ - An instance of a Logger class (http transport information will be logged here) - defaults to nil
13
+ #
14
+ # === Examples
15
+ # client = TMS::Client.new("foo@example.com", "onetwothree", {
16
+ # :api_root => "https://tms.govdelivery.com",
17
+ # :logger => Logger.new(STDOUT)})
18
+ #
19
+ def initialize(username, password, options = {:api_root => 'http://localhost:3000', :logger => nil})
20
+ @api_root = options[:api_root]
21
+ connect!(username, password, options[:logger])
22
+ discover!
23
+ end
24
+
25
+ def connect!(username, password, logger)
26
+ self.connection = TMS::Connection.new(:username => username, :password => password, :api_root => @api_root, :logger => logger)
27
+ end
28
+
29
+ def discover!
30
+ services = get('/').body
31
+ parse_links(services['_links'])
32
+ end
33
+
34
+ def get(href)
35
+ response = raw_connection.get(href)
36
+ case response.status
37
+ when 401..499
38
+ raise TMS::Request::Error.new(response.status)
39
+ when 202
40
+ raise TMS::Request::InProgress.new(response.body['message'])
41
+ else
42
+ return response
43
+ end
44
+ end
45
+
46
+ def post(obj)
47
+ raw_connection.post do |req|
48
+ req.url @api_root + obj.href
49
+ req.headers['Content-Type'] = 'application/json'
50
+ req.body = obj.to_json
51
+ end
52
+ end
53
+
54
+ def put(obj)
55
+ raw_connection.put do |req|
56
+ req.url @api_root + obj.href
57
+ req.headers['Content-Type'] = 'application/json'
58
+ req.body = obj.to_json
59
+ end
60
+ end
61
+
62
+ def delete(href)
63
+ response = raw_connection.delete(href)
64
+ case response.status
65
+ when 200
66
+ return response
67
+ else
68
+ raise TMS::Request::Error.new(response.status)
69
+ end
70
+ end
71
+
72
+ def raw_connection
73
+ connection.connection
74
+ end
75
+
76
+ def client
77
+ self
78
+ end
79
+
80
+ end
@@ -0,0 +1,53 @@
1
+ module TMS::CollectionResource
2
+ def self.included(base)
3
+ base.send(:include, InstanceMethods)
4
+ end
5
+
6
+ module InstanceMethods
7
+ include TMS::Base
8
+ attr_accessor :collection
9
+
10
+ def initialize(client, href, items=nil)
11
+ super(client, href)
12
+ if items
13
+ initialize_collection_from_items(items)
14
+ else
15
+ self.collection = []
16
+ end
17
+
18
+ end
19
+
20
+ def get
21
+ response = client.get(href)
22
+ initialize_collection_from_items(response.body)
23
+ #setup page links from header
24
+ links = LinkHeader.parse(response.headers['link']).to_a.collect do |a|
25
+ {a[1][0].last => a[0]}
26
+ end
27
+ parse_links(links)
28
+ self
29
+ end
30
+
31
+ def build(attributes=nil)
32
+ thing = instance_class(self.class).new(client, self.href, attributes || {})
33
+ self.collection << thing
34
+ thing
35
+ end
36
+
37
+ def to_json
38
+ @collection.map(&:to_json)
39
+ end
40
+
41
+ def to_s
42
+ "<#{self.class.inspect} href=#{self.href} collection=#{self.collection.inspect}>"
43
+ end
44
+
45
+ private
46
+
47
+ def initialize_collection_from_items(items)
48
+ self.collection = items.map do |attrs|
49
+ instance_class(self.class).new(client, nil, attrs)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,30 @@
1
+ class TMS::Connection
2
+ attr_accessor :username, :password, :api_root, :connection, :logger
3
+
4
+ def get(href)
5
+ resp = connection.get("#{href}.json")
6
+ if resp.status != 200
7
+ raise RecordNotFound.new("Could not find resource at #{href} (status #{resp.status})")
8
+ else
9
+ resp.body
10
+ end
11
+ end
12
+
13
+ def initialize(opts={})
14
+ self.username = opts[:username]
15
+ self.password = opts[:password]
16
+ self.api_root = opts[:api_root]
17
+ self.logger = opts[:logger]
18
+ setup_connection
19
+ end
20
+
21
+ def setup_connection
22
+ self.connection = Faraday.new(:url => self.api_root) do |faraday|
23
+ faraday.use TMS::Logger, self.logger if self.logger
24
+ faraday.request :json
25
+ faraday.basic_auth(self.username, self.password)
26
+ faraday.response :json, :content_type => /\bjson$/
27
+ faraday.adapter :net_http
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,175 @@
1
+ module TMS::InstanceResource
2
+ def self.included(base)
3
+ base.send(:include, TMS::Base)
4
+ base.extend(ClassMethods)
5
+ base.send(:include, InstanceMethods)
6
+ end
7
+
8
+ module ClassMethods
9
+ #
10
+ # Writeable attributes are sent on POST/PUT.
11
+ #
12
+ def writeable_attributes(*attrs)
13
+ @writeable_attributes ||= []
14
+ if attrs.any?
15
+ @writeable_attributes.map!(&:to_sym).concat(attrs).uniq! if attrs.any?
16
+ setup_attributes(@writeable_attributes, false)
17
+ end
18
+ @writeable_attributes
19
+ end
20
+
21
+ #
22
+ # Readonly attributes don't get POSTed.
23
+ # (timestamps are included by default)
24
+ #
25
+ def readonly_attributes(*attrs)
26
+ @readonly_attributes ||= [:created_at, :updated_at, :completed_at]
27
+ if attrs.any?
28
+ @readonly_attributes.map!(&:to_sym).concat(attrs).uniq!
29
+ setup_attributes(@readonly_attributes, true)
30
+ end
31
+ @readonly_attributes
32
+ end
33
+
34
+ #
35
+ # For collections that are represented as attributes (i.e. inline, no href)
36
+ #
37
+ # # message.rb
38
+ # collection_attributes :recipients
39
+ def collection_attributes(*attrs)
40
+ @collection_attributes ||= []
41
+ if attrs.any?
42
+ @collection_attributes.map!(&:to_sym).concat(attrs).uniq! if attrs.any?
43
+ @collection_attributes.each { |a| setup_collection(a) }
44
+ end
45
+ @collection_attributes
46
+ end
47
+
48
+ def custom_class_names
49
+ @custom_class_names ||= {}
50
+ end
51
+
52
+ #
53
+ # For collections that are represented as attributes (i.e. inline, no href)
54
+ # and that have a class name other than the one we would infer.
55
+ #
56
+ # # email.rb
57
+ # collection_attributes :recipients, 'EmailRecipient'
58
+ def collection_attribute(attr, tms_class)
59
+ @collection_attributes ||= []
60
+ @collection_attributes.push(attr).uniq!
61
+ setup_collection(attr, TMS.const_get(tms_class))
62
+ end
63
+
64
+ def setup_attributes(attrs, readonly=false)
65
+ attrs.map(&:to_sym).each do |property|
66
+ self.send :define_method, :"#{property}=", &lambda { |v| @attributes[property] = v } unless readonly
67
+ self.send :define_method, property.to_sym, &lambda { @attributes[property] }
68
+ end
69
+ end
70
+
71
+ def setup_collection(property, klass=nil)
72
+ if klass
73
+ custom_class_names[property] = klass
74
+ else
75
+ klass ||= TMS.const_get(property.to_s.capitalize)
76
+ end
77
+
78
+ self.send :define_method, property.to_sym, &lambda { @attributes[property] ||= klass.new(self.client, nil, nil) }
79
+ end
80
+ end
81
+
82
+ module InstanceMethods
83
+ def initialize(client, href=nil, attrs=nil)
84
+ super(client, href)
85
+ @attributes = {}
86
+ attrs ||= self.client.get(href).body if href
87
+ set_attributes_from_hash(attrs) if attrs
88
+ end
89
+
90
+ def get
91
+ set_attributes_from_hash(self.client.get(href).body)
92
+ self
93
+ end
94
+
95
+ def post
96
+ response = client.post(self)
97
+ case response.status
98
+ when 201
99
+ set_attributes_from_hash(response.body)
100
+ return true
101
+ when 401
102
+ raise Exception.new("401 Not Authorized")
103
+ when 404
104
+ raise(Exception.new("Can't POST to #{self.href}"))
105
+ else
106
+ if response.body['errors']
107
+ self.errors = response.body['errors']
108
+ end
109
+ end
110
+ return false
111
+ end
112
+
113
+ def put
114
+ response = client.put(self)
115
+ case response.status
116
+ when 200
117
+ set_attributes_from_hash(response.body)
118
+ return true
119
+ when 401
120
+ raise Exception.new("401 Not Authorized")
121
+ when 404
122
+ raise(Exception.new("Can't POST to #{self.href}"))
123
+ else
124
+ if response.body['errors']
125
+ self.errors = response.body['errors']
126
+ end
127
+ end
128
+ return false
129
+ end
130
+
131
+ def delete
132
+ response = self.client.delete(href)
133
+ case response.status
134
+ when 200
135
+ return true
136
+ else
137
+ if response.body['errors']
138
+ self.errors = response.body['errors']
139
+ end
140
+ end
141
+ return false
142
+ end
143
+
144
+ def to_s
145
+ "<#{self.class.inspect}#{' href=' + self.href if self.href} attributes=#{@attributes.inspect}>"
146
+ end
147
+
148
+ def to_json
149
+ json_hash = {}
150
+ self.class.writeable_attributes.each do |attr|
151
+ json_hash[attr] = self.send(attr)
152
+ end
153
+ self.class.collection_attributes.each do |coll|
154
+ json_hash[coll] = self.send(coll).to_json
155
+ end
156
+ json_hash
157
+ end
158
+
159
+ private
160
+
161
+ def set_attributes_from_hash(hash)
162
+ hash.reject { |k, _| k=~/^_/ }.each do |property, value|
163
+ if self.class.collection_attributes.include?(property.to_sym)
164
+ klass = self.class.custom_class_names[property] || TMS.const_get(property.to_s.capitalize)
165
+ @attributes[property.to_sym] = klass.new(client, nil, value)
166
+ else
167
+ @attributes[property.to_sym] = value
168
+ end
169
+ end
170
+ self.errors = hash['errors']
171
+ parse_links(hash['_links'])
172
+ end
173
+
174
+ end
175
+ end
@@ -0,0 +1,34 @@
1
+ module TMS #:nodoc:
2
+ class Logger < Faraday::Response::Middleware #:nodoc:
3
+ extend Forwardable
4
+
5
+ def initialize(app, logger = nil)
6
+ super(app)
7
+ @logger = logger || begin
8
+ require 'logger'
9
+ ::Logger.new(STDOUT)
10
+ end
11
+ end
12
+
13
+ def_delegators :@logger, :debug, :info, :warn, :error, :fatal
14
+
15
+ def call(env)
16
+ puts env.inspect
17
+ info "#{env[:method]} #{env[:url].to_s}"
18
+ debug('request') { dump_headers env[:request_headers] }
19
+ super
20
+ end
21
+
22
+ def on_complete(env)
23
+ info('Status') { env[:status].to_s }
24
+ debug('response headers') { dump_headers env[:response_headers] }
25
+ debug('response body') { env[:body].inspect }
26
+ end
27
+
28
+ private
29
+
30
+ def dump_headers(headers)
31
+ headers.map { |k, v| "#{k}: #{v.inspect}" }.join("\n")
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,17 @@
1
+ module TMS #:nodoc:
2
+ module Request
3
+ # The generic TMS error class
4
+ class Error < StandardError
5
+ attr_reader :code
6
+
7
+ def initialize(code)
8
+ super("HTTP Error: #{code}")
9
+ @code=code
10
+ end
11
+ end
12
+
13
+ # Raised when a recipient list is still being constructed and a request is made to view the
14
+ # recipient list for a message.
15
+ class InProgress < StandardError; end
16
+ end
17
+ end
@@ -0,0 +1,49 @@
1
+ class TMS::Emails
2
+ include TMS::CollectionResource
3
+ end
4
+
5
+ class TMS::VoiceMessages
6
+ include TMS::CollectionResource
7
+ end
8
+
9
+ class TMS::SmsMessages
10
+ include TMS::CollectionResource
11
+ end
12
+
13
+ class TMS::Emails
14
+ include TMS::CollectionResource
15
+ end
16
+
17
+ class TMS::Recipients
18
+ include TMS::CollectionResource
19
+ end
20
+
21
+ class TMS::EmailRecipients
22
+ include TMS::CollectionResource
23
+ end
24
+
25
+ # A collection of Keyword objects.
26
+ #
27
+ # === Example
28
+ # keywords = client.keywords.get
29
+ #
30
+ class TMS::Keywords
31
+ include TMS::CollectionResource
32
+ end
33
+
34
+ class TMS::InboundMessages
35
+ include TMS::CollectionResource
36
+ end
37
+
38
+ # A collection of CommandType instances.
39
+ # This resource changes infrequently. It may be used to dynamically construct a
40
+ # user interface for configuring arbitrary SMS keywords for an account.
41
+ #
42
+ # This resource is read-only.
43
+ #
44
+ # === Example
45
+ # client.command_types.get
46
+ # client.command_types.collection.each {|at| ... }
47
+ class TMS::CommandTypes
48
+ include TMS::CollectionResource
49
+ end
@@ -0,0 +1,24 @@
1
+ module TMS #:nodoc:
2
+ # A command is a combination of behavior and parameters that should be executed
3
+ # when an incoming SMS message matches the associated Keyword.
4
+ #
5
+ # ==== Attributes
6
+ #
7
+ # * +name+ - The name of the command. This will default to the command_type if not supplied.
8
+ # * +command_type+ - The type of this command. A list of valid types can be found by querying the CommandType list.
9
+ # * +params+ - A Hash of string/string pairs used as configuration for this command.
10
+ #
11
+ # === Examples
12
+ # command = keyword.commands.build(:name => "subscribe to news", :command_type => "dcm_subscribe", :dcm_account_code => "NEWS", :dcm_topic_codes => "NEWS_1, NEWS_2")
13
+ # command.post
14
+ # command.dcm_topic_codes += ", NEWS_5"
15
+ # command.put
16
+ # command.delete
17
+ class Command
18
+ include InstanceResource
19
+
20
+ writeable_attributes :name, :command_type, :params
21
+ readonly_attributes :created_at, :updated_at
22
+
23
+ end
24
+ end
@@ -0,0 +1,19 @@
1
+ module TMS #:nodoc:
2
+ # CommandType is a pair of values (name, fields) that can be attached
3
+ # to a Keyword (in a Command object).
4
+ #
5
+ # This resource is read-only.
6
+ #
7
+ # ==== Attributes
8
+ #
9
+ # * +name+ - The name of the CommandType.
10
+ # * +fields+ - An Array of strings representing the different fields on this
11
+ # CommandType. Field values will always be strings.
12
+ #
13
+ class CommandType
14
+
15
+ include InstanceResource
16
+
17
+ readonly_attributes :name, :fields
18
+ end
19
+ end
@@ -0,0 +1,3 @@
1
+ class TMS::Commands
2
+ include TMS::CollectionResource
3
+ end
@@ -0,0 +1,9 @@
1
+ module TMS #:nodoc:
2
+ class Email
3
+ include InstanceResource
4
+
5
+ writeable_attributes :body, :from, :subject
6
+ readonly_attributes :created_at, :completed_at
7
+ collection_attribute :recipients, 'EmailRecipients'
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ module TMS #:nodoc:
2
+ class EmailRecipient
3
+ include InstanceResource
4
+
5
+ writeable_attributes :email
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module TMS #:nodoc:
2
+ class InboundMessage
3
+ include InstanceResource
4
+
5
+ readonly_attributes :created_at, :completed_at, :from, :body, :to
6
+ end
7
+ end