govdelivery-tms 0.8.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.
Files changed (65) hide show
  1. checksums.yaml +15 -0
  2. data/Gemfile +14 -0
  3. data/README.md +324 -0
  4. data/Rakefile +20 -0
  5. data/govdelivery-tms.gemspec +30 -0
  6. data/lib/govdelivery-tms.rb +43 -0
  7. data/lib/govdelivery-tms/base.rb +37 -0
  8. data/lib/govdelivery-tms/client.rb +97 -0
  9. data/lib/govdelivery-tms/collection_resource.rb +54 -0
  10. data/lib/govdelivery-tms/connection.rb +34 -0
  11. data/lib/govdelivery-tms/errors.rb +58 -0
  12. data/lib/govdelivery-tms/instance_resource.rb +219 -0
  13. data/lib/govdelivery-tms/link_header.rb +223 -0
  14. data/lib/govdelivery-tms/logger.rb +36 -0
  15. data/lib/govdelivery-tms/mail/delivery_method.rb +63 -0
  16. data/lib/govdelivery-tms/resource/collections.rb +98 -0
  17. data/lib/govdelivery-tms/resource/command.rb +25 -0
  18. data/lib/govdelivery-tms/resource/command_action.rb +17 -0
  19. data/lib/govdelivery-tms/resource/command_type.rb +20 -0
  20. data/lib/govdelivery-tms/resource/email_message.rb +81 -0
  21. data/lib/govdelivery-tms/resource/email_recipient.rb +31 -0
  22. data/lib/govdelivery-tms/resource/email_recipient_click.rb +8 -0
  23. data/lib/govdelivery-tms/resource/email_recipient_open.rb +8 -0
  24. data/lib/govdelivery-tms/resource/email_template.rb +15 -0
  25. data/lib/govdelivery-tms/resource/from_address.rb +10 -0
  26. data/lib/govdelivery-tms/resource/inbound_sms_message.rb +8 -0
  27. data/lib/govdelivery-tms/resource/ipaws_acknowledgement.rb +9 -0
  28. data/lib/govdelivery-tms/resource/ipaws_alert.rb +38 -0
  29. data/lib/govdelivery-tms/resource/ipaws_category.rb +7 -0
  30. data/lib/govdelivery-tms/resource/ipaws_cog_profile.rb +29 -0
  31. data/lib/govdelivery-tms/resource/ipaws_event_code.rb +7 -0
  32. data/lib/govdelivery-tms/resource/ipaws_nwem_area.rb +18 -0
  33. data/lib/govdelivery-tms/resource/ipaws_nwem_authorization.rb +9 -0
  34. data/lib/govdelivery-tms/resource/ipaws_nwem_auxilary_data.rb +8 -0
  35. data/lib/govdelivery-tms/resource/ipaws_response_type.rb +7 -0
  36. data/lib/govdelivery-tms/resource/ipaws_static_resource.rb +8 -0
  37. data/lib/govdelivery-tms/resource/keyword.rb +30 -0
  38. data/lib/govdelivery-tms/resource/recipient.rb +10 -0
  39. data/lib/govdelivery-tms/resource/sms_message.rb +35 -0
  40. data/lib/govdelivery-tms/resource/webhook.rb +20 -0
  41. data/lib/govdelivery-tms/util/core_ext.rb +27 -0
  42. data/lib/govdelivery-tms/util/hal_link_parser.rb +50 -0
  43. data/lib/govdelivery-tms/version.rb +3 -0
  44. data/spec/client_spec.rb +41 -0
  45. data/spec/command_types_spec.rb +29 -0
  46. data/spec/email_message_spec.rb +102 -0
  47. data/spec/email_template_spec.rb +149 -0
  48. data/spec/errors_spec.rb +13 -0
  49. data/spec/from_address_spec.rb +86 -0
  50. data/spec/inbound_sms_messages_spec.rb +19 -0
  51. data/spec/instance_resource_spec.rb +61 -0
  52. data/spec/ipaws_acknowledgement_spec.rb +16 -0
  53. data/spec/ipaws_alerts_spec.rb +192 -0
  54. data/spec/ipaws_cog_profile_spec.rb +75 -0
  55. data/spec/ipaws_event_codes_spec.rb +35 -0
  56. data/spec/ipaws_nwem_areas_spec.rb +58 -0
  57. data/spec/ipaws_nwem_authorization_spec.rb +16 -0
  58. data/spec/keyword_spec.rb +62 -0
  59. data/spec/keywords_spec.rb +21 -0
  60. data/spec/mail/delivery_method_spec.rb +52 -0
  61. data/spec/sms_message_spec.rb +63 -0
  62. data/spec/sms_messages_spec.rb +21 -0
  63. data/spec/spec_helper.rb +31 -0
  64. data/spec/tms_spec.rb +7 -0
  65. metadata +172 -0
@@ -0,0 +1,37 @@
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, :new_record
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
+
25
+ def new_record?
26
+ !!self.new_record
27
+ end
28
+
29
+ def href=(href)
30
+ self.new_record=false
31
+ @href=href
32
+ end
33
+ end
34
+
35
+ end
36
+
37
+ end
@@ -0,0 +1,97 @@
1
+ require 'logger'
2
+ # The client class to connect and talk to the TMS REST API.
3
+ class TMS::Client
4
+ include TMS::Util::HalLinkParser
5
+ include TMS::CoreExt
6
+
7
+ attr_accessor :connection, :href, :api_root, :logger
8
+
9
+ DEFAULTS = {:api_root => 'https://tms.govdelivery.com', :logger => nil}.freeze
10
+
11
+ # Create a new client and issue a request for the available resources for a given account.
12
+ #
13
+ # @param [String] auth_token The auth_token of your account
14
+ # @param [Hash] options
15
+ # @option options [String] :api_root The root URL of the TMS api. Defaults to localhost:3000
16
+ # @option options [Logger] :logger An instance of a Logger class (http transport information will be logged here) - defaults to nil
17
+ #
18
+ # @example
19
+ # client = TMS::Client.new("auth_token", {
20
+ # :api_root => "https://tms.govdelivery.com",
21
+ # :logger => Logger.new(STDOUT)})
22
+ # client = TMS::Client.new("auth_token", {
23
+ # api_root: "https://tms.govdelivery.com",
24
+ # logger: false})
25
+ def initialize(auth_token, options = DEFAULTS)
26
+ @api_root = options[:api_root]
27
+ @logger = options.fetch(:logger, setup_logging(options[:debug]))
28
+ connect!(auth_token, options.except(:api_root, :logger, :debug))
29
+ discover!
30
+ end
31
+
32
+ def connect!(auth_token, options={})
33
+ self.connection = TMS::Connection.new({:auth_token => auth_token, :api_root => api_root, :logger => logger}.merge!(options))
34
+ end
35
+
36
+ def discover!
37
+ services = get('/').body
38
+ parse_links(services['_links'])
39
+ end
40
+
41
+ def get(href)
42
+ response = raw_connection.get(href)
43
+ case response.status
44
+ when 500..599
45
+ raise TMS::Request::Error.new(response.status)
46
+ when 401..499
47
+ raise TMS::Request::Error.new(response.status)
48
+ when 202
49
+ raise TMS::Request::InProgress.new(response.body['message'])
50
+ else
51
+ return response
52
+ end
53
+ end
54
+
55
+ def post(obj)
56
+ raw_connection.post do |req|
57
+ req.url @api_root + obj.href
58
+ req.headers['Content-Type'] = 'application/json'
59
+ req.body = obj.to_json
60
+ end
61
+ end
62
+
63
+ def put(obj)
64
+ raw_connection.put do |req|
65
+ req.url @api_root + obj.href
66
+ req.headers['Content-Type'] = 'application/json'
67
+ req.body = obj.to_json
68
+ end
69
+ end
70
+
71
+ def delete(href)
72
+ response = raw_connection.delete(href)
73
+ case response.status
74
+ when 200...299
75
+ return response
76
+ else
77
+ raise TMS::Request::Error.new(response.status)
78
+ end
79
+ end
80
+
81
+ def raw_connection
82
+ connection.connection
83
+ end
84
+
85
+ def client
86
+ self
87
+ end
88
+
89
+ private
90
+
91
+ def setup_logging(debug)
92
+ logger = Logger.new(STDOUT)
93
+ logger.level = debug ? Logger::DEBUG : Logger::INFO
94
+ logger
95
+ end
96
+
97
+ end
@@ -0,0 +1,54 @@
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
+ thing.new_record = true
34
+ self.collection << thing
35
+ thing
36
+ end
37
+
38
+ def to_json
39
+ @collection.map(&:to_json)
40
+ end
41
+
42
+ def to_s
43
+ "<#{self.class.inspect} href=#{self.href} collection=#{self.collection.inspect}>"
44
+ end
45
+
46
+ private
47
+
48
+ def initialize_collection_from_items(items)
49
+ self.collection = items.map do |attrs|
50
+ instance_class(self.class).new(client, nil, attrs)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,34 @@
1
+ class TMS::Connection
2
+ attr_accessor :auth_token, :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.auth_token = opts[:auth_token]
15
+ self.api_root = opts[:api_root]
16
+ self.logger = opts[:logger]
17
+ setup_connection
18
+ end
19
+
20
+ def setup_connection
21
+ self.connection = Faraday.new(:url => self.api_root) do |faraday|
22
+ faraday.use TMS::Logger, self.logger if self.logger
23
+ faraday.request :json
24
+ faraday.headers['X-AUTH-TOKEN'] = auth_token
25
+ faraday.headers[:user_agent] = "GovDelivery Ruby TMS::Client #{TMS::VERSION}"
26
+ faraday.response :json, :content_type => /\bjson$/
27
+ faraday.adapter :net_http
28
+ end
29
+ end
30
+
31
+ def dump_headers(headers)
32
+ headers.map { |k, v| "#{k}: #{v.inspect}" }.join("\n")
33
+ end
34
+ end
@@ -0,0 +1,58 @@
1
+ module TMS
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
+
18
+ module Errors
19
+ class ServerError < StandardError
20
+ def initialize(response)
21
+ super("TMS client encountered a server error: #{response.status} \n#{response.body}")
22
+ end
23
+ end
24
+ class NoRelation < StandardError
25
+ def initialize(rel=nil, obj=nil)
26
+ message = "no link relation "
27
+ message << "'#{rel}' " if rel
28
+ message << 'is available'
29
+ message << " for #{obj}" if obj
30
+ super(message)
31
+ end
32
+ end
33
+ class InvalidVerb < StandardError
34
+ attr_reader :record
35
+
36
+ def initialize(record_or_string)
37
+ if record_or_string.respond_to?(:href)
38
+ @record = record_or_string
39
+ super("Couldn't POST #{record.class} to #{record.href}: #{record.errors.map { |k, v| "#{k} #{v.join(' and ')}" }.join(', ')}")
40
+ else
41
+ super(record_or_string)
42
+ end
43
+
44
+ end
45
+ end
46
+ class InvalidPost < InvalidVerb
47
+ end
48
+ class InvalidPut < InvalidVerb
49
+ end
50
+ class InvalidDelete < InvalidVerb
51
+ end
52
+ class InvalidGet < StandardError
53
+ def initialize(message=nil)
54
+ super(message || "Can't GET a resource after an invalid POST; either create a new object or fix errors")
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,219 @@
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
+ attr_accessor :response
9
+
10
+ module ClassMethods
11
+ ##
12
+ # Writeable attributes are sent on POST/PUT.
13
+ #
14
+ def writeable_attributes(*attrs)
15
+ @writeable_attributes ||= []
16
+ if attrs.any?
17
+ @writeable_attributes.map!(&:to_sym).concat(attrs).uniq! if attrs.any?
18
+ setup_attributes(@writeable_attributes, false)
19
+ end
20
+ @writeable_attributes
21
+ end
22
+
23
+ ##
24
+ # Linkable attributes are sent on POST/PUT.
25
+ #
26
+ def linkable_attributes(*attrs)
27
+ @linkable_attributes ||= []
28
+ if attrs.any?
29
+ @linkable_attributes.map!(&:to_sym).concat(attrs).uniq! if attrs.any?
30
+ end
31
+ @linkable_attributes
32
+ end
33
+
34
+ ##
35
+ # Readonly attributes don't get POSTed.
36
+ # (timestamps are included by default)
37
+ #
38
+ def readonly_attributes(*attrs)
39
+ @readonly_attributes ||= [:created_at, :updated_at, :completed_at]
40
+ if attrs.any?
41
+ @readonly_attributes.map!(&:to_sym).concat(attrs).uniq!
42
+ setup_attributes(@readonly_attributes, true)
43
+ end
44
+ @readonly_attributes
45
+ end
46
+
47
+ ##
48
+ # For collections that are represented as attributes (i.e. inline, no href)
49
+ #
50
+ # @example
51
+ # collection_attributes :recipients
52
+ #
53
+ def collection_attributes(*attrs)
54
+ @collection_attributes ||= []
55
+ if attrs.any?
56
+ @collection_attributes.map!(&:to_sym).concat(attrs).uniq!
57
+ @collection_attributes.each { |a| setup_collection(a) }
58
+ end
59
+ @collection_attributes
60
+ end
61
+
62
+ def custom_class_names
63
+ @custom_class_names ||= {}
64
+ end
65
+
66
+ ##
67
+ # For collections that are represented as attributes (i.e. inline, no href)
68
+ # and that have a class name other than the one we would infer.
69
+ #
70
+ # @example
71
+ # collection_attributes :recipients, 'EmailRecipient'
72
+ #
73
+ def collection_attribute(attr, tms_class)
74
+ @collection_attributes ||= []
75
+ @collection_attributes.push(attr).uniq!
76
+ setup_collection(attr, TMS.const_get(tms_class))
77
+ end
78
+
79
+ ##
80
+ # Read-only collection attributes don't get POSTed.
81
+ # Use this for collections that are represented as attributes, but cannot be modified.
82
+ #
83
+ # @example
84
+ # readonly_collection_attribute :opens
85
+ #
86
+ def readonly_collection_attribute(attr, tms_class)
87
+ @readonly_collection_attributes ||= []
88
+ @readonly_collection_attributes.push(attr).uniq!
89
+ setup_collection(attr, TMS.const_get(tms_class))
90
+ end
91
+
92
+ def setup_attributes(attrs, readonly=false)
93
+ attrs.map(&:to_sym).each do |property|
94
+ self.send :define_method, :"#{property}=", &lambda { |v| @attributes[property] = v } unless readonly
95
+ self.send :define_method, property.to_sym, &lambda { @attributes[property] }
96
+ end
97
+ end
98
+
99
+ def setup_collection(property, klass=nil)
100
+ if klass
101
+ custom_class_names[property] = klass
102
+ else
103
+ klass ||= TMS.const_get(property.to_s.capitalize)
104
+ end
105
+
106
+ self.send :define_method, property.to_sym, &lambda { @attributes[property] ||= klass.new(self.client, nil, nil) }
107
+ end
108
+ end
109
+
110
+ module InstanceMethods
111
+ attr_reader :links
112
+
113
+ def initialize(client, href=nil, attrs=nil)
114
+ super(client, href)
115
+ @attributes = {}
116
+ @links = {}
117
+ set_attributes_from_hash(attrs) if attrs
118
+ end
119
+
120
+ def attributes
121
+ @attributes
122
+ end
123
+
124
+ def get
125
+ raise TMS::Errors::InvalidGet if self.new_record?
126
+ process_response(client.get(self.href), :get) && self
127
+ end
128
+ alias_method :get!, :get
129
+
130
+ def post
131
+ self.errors = nil
132
+ process_response(client.post(self), :post)
133
+ end
134
+
135
+ def post!
136
+ self.post or raise TMS::Errors::InvalidPost.new(self)
137
+ end
138
+
139
+ def put
140
+ process_response(client.put(self), :put)
141
+ end
142
+
143
+ def put!
144
+ process_response(client.put(self), :put) or raise TMS::Errors::InvalidPut.new(self)
145
+ end
146
+
147
+ def delete
148
+ process_response(client.delete(self.href), :delete)
149
+ end
150
+
151
+ def delete!
152
+ process_response(client.delete(self.href), :delete) or raise TMS::Errors::InvalidDelete.new(self)
153
+ end
154
+
155
+ def to_s
156
+ "<#{self.class.inspect}#{' href=' + self.href if self.href} attributes=#{@attributes.inspect}>"
157
+ end
158
+
159
+ def to_json
160
+ json_hash = {}
161
+ self.class.writeable_attributes.each do |attr|
162
+ json_hash[attr] = self.send(attr)
163
+ end
164
+ self.class.collection_attributes.each do |coll|
165
+ json_hash[coll] = self.send(coll).to_json
166
+ end
167
+ self.class.linkable_attributes.each do |attr|
168
+ json_hash[:_links] ||= {}
169
+ json_hash[:_links][attr] = @links[attr]
170
+ end
171
+ json_hash
172
+ end
173
+
174
+ protected
175
+
176
+ def relation_class(rel)
177
+ self.class.custom_class_names[rel.to_sym] || super
178
+ end
179
+
180
+ def process_response(response, method)
181
+ self.response = response
182
+ error_class = TMS::Errors.const_get("Invalid#{method.to_s.capitalize}")
183
+ case response.status
184
+ when 204
185
+ return true
186
+ when 200..299
187
+ set_attributes_from_hash(response.body) if response.body.is_a?(Hash)
188
+ @links = {}
189
+ self.new_record=false
190
+ return true
191
+ when 401
192
+ raise error_class.new("401 Not Authorized")
193
+ when 404
194
+ raise(error_class.new("Can't POST to #{self.href}"))
195
+ when 500..599
196
+ raise(TMS::Errors::ServerError.new(response))
197
+ else # 422?
198
+ if response.body['errors']
199
+ self.errors = response.body['errors']
200
+ end
201
+ end
202
+ return false
203
+ end
204
+
205
+ def set_attributes_from_hash(hash)
206
+ hash.reject { |k, _| k=~/^_/ }.each do |property, value|
207
+ if self.class.collection_attributes.include?(property.to_sym)
208
+ klass = self.class.custom_class_names[property] || TMS.const_get(property.to_s.capitalize)
209
+ @attributes[property.to_sym] = klass.new(client, nil, value)
210
+ else
211
+ @attributes[property.to_sym] = value
212
+ end
213
+ end
214
+ self.errors = hash['errors']
215
+ parse_links(hash['_links'])
216
+ end
217
+
218
+ end
219
+ end