screenbeacon 0.1.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 (41) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.travis.yml +29 -0
  4. data/Gemfile +8 -0
  5. data/LICENSE +21 -0
  6. data/README.rdoc +44 -0
  7. data/Rakefile +7 -0
  8. data/VERSION +1 -0
  9. data/bin/screenbeacon-console +7 -0
  10. data/gemfiles/default-with-activesupport.gemfile +10 -0
  11. data/gemfiles/json.gemfile +12 -0
  12. data/gemfiles/yajl.gemfile +12 -0
  13. data/lib/data/ca-certificates.crt +5165 -0
  14. data/lib/screenbeacon/alert.rb +32 -0
  15. data/lib/screenbeacon/api_operations/create.rb +16 -0
  16. data/lib/screenbeacon/api_operations/delete.rb +11 -0
  17. data/lib/screenbeacon/api_operations/list.rb +17 -0
  18. data/lib/screenbeacon/api_operations/request.rb +42 -0
  19. data/lib/screenbeacon/api_operations/update.rb +17 -0
  20. data/lib/screenbeacon/api_resource.rb +35 -0
  21. data/lib/screenbeacon/errors/api_connection_error.rb +4 -0
  22. data/lib/screenbeacon/errors/api_error.rb +4 -0
  23. data/lib/screenbeacon/errors/authentication_error.rb +4 -0
  24. data/lib/screenbeacon/errors/invalid_request_error.rb +10 -0
  25. data/lib/screenbeacon/errors/screenbeacon_error.rb +20 -0
  26. data/lib/screenbeacon/project.rb +13 -0
  27. data/lib/screenbeacon/screenbeacon_object.rb +263 -0
  28. data/lib/screenbeacon/test.rb +8 -0
  29. data/lib/screenbeacon/util.rb +130 -0
  30. data/lib/screenbeacon/version.rb +3 -0
  31. data/lib/screenbeacon.rb +305 -0
  32. data/screenbeacon.gemspec +27 -0
  33. data/test/screenbeacon/alert_test.rb +14 -0
  34. data/test/screenbeacon/api_resource_test.rb +82 -0
  35. data/test/screenbeacon/project_test.rb +36 -0
  36. data/test/screenbeacon/screenbeacon_object_test.rb +28 -0
  37. data/test/screenbeacon/test_test.rb +36 -0
  38. data/test/screenbeacon/util_test.rb +34 -0
  39. data/test/test_data.rb +147 -0
  40. data/test/test_helper.rb +43 -0
  41. metadata +176 -0
@@ -0,0 +1,32 @@
1
+ module Screenbeacon
2
+ class Alert < APIResource
3
+ include Screenbeacon::APIOperations::List
4
+
5
+ def resolve(opts={})
6
+ response, opts = request(:patch, resolve_url, {}, opts)
7
+ refresh_from(response, opts)
8
+ end
9
+
10
+ # Resolve all alerts on account
11
+ def self.resolve_all(opts={})
12
+ response, opts = request(:post, resolve_all_url, {}, opts)
13
+ refresh_from(response, opts)
14
+ end
15
+
16
+ def self.resolve_all(filters={}, opts={})
17
+ response, opts = request(:post, resolve_all_url, filters, opts)
18
+ Util.convert_to_screenbeacon_object(response, opts)
19
+ end
20
+
21
+ private
22
+
23
+ def resolve_url
24
+ url + '/resolve'
25
+ end
26
+
27
+ def self.resolve_all_url
28
+ url + '/resolve'
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,16 @@
1
+ module Screenbeacon
2
+ module APIOperations
3
+ module Create
4
+ module ClassMethods
5
+ def create(params={}, opts={})
6
+ response, opts = request(:post, url, params, opts)
7
+ Util.convert_to_screenbeacon_object(response, opts)
8
+ end
9
+ end
10
+
11
+ def self.included(base)
12
+ base.extend(ClassMethods)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,11 @@
1
+ module Screenbeacon
2
+ module APIOperations
3
+ module Delete
4
+ def delete(params={}, opts={})
5
+ opts = Util.normalize_opts(opts)
6
+ response, opts = request(:delete, url, params, opts)
7
+ refresh_from(response, opts)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+ module Screenbeacon
2
+ module APIOperations
3
+ module List
4
+ module ClassMethods
5
+ def all(filters={}, opts={})
6
+ opts = Util.normalize_opts(opts)
7
+ response, opts = request(:get, url, filters, opts)
8
+ Util.convert_to_screenbeacon_object(response, opts)
9
+ end
10
+ end
11
+
12
+ def self.included(base)
13
+ base.extend(ClassMethods)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,42 @@
1
+ module Screenbeacon
2
+ module APIOperations
3
+ module Request
4
+ module ClassMethods
5
+ OPTS_KEYS_TO_PERSIST = Set[:api_id, :api_token, :api_base, :screenbeacon_version]
6
+
7
+ def request(method, url, params={}, opts={})
8
+ opts = Util.normalize_opts(opts)
9
+
10
+ headers = opts.clone
11
+ api_id = headers.delete(:api_id)
12
+ api_token = headers.delete(:api_token)
13
+ api_base = headers.delete(:api_base)
14
+ # Assume all remaining opts must be headers
15
+
16
+ response, opts[:api_id], opts[:api_token] = Screenbeacon.request(method, url, api_id, api_token, params, headers, api_base)
17
+
18
+ # Hash#select returns an array before 1.9
19
+ opts_to_persist = {}
20
+ opts.each do |k, v|
21
+ if OPTS_KEYS_TO_PERSIST.include?(k)
22
+ opts_to_persist[k] = v
23
+ end
24
+ end
25
+
26
+ [response, opts_to_persist]
27
+ end
28
+ end
29
+
30
+ def self.included(base)
31
+ base.extend(ClassMethods)
32
+ end
33
+
34
+ protected
35
+
36
+ def request(method, url, params={}, opts={})
37
+ opts = @opts.merge(Util.normalize_opts(opts))
38
+ self.class.request(method, url, params, opts)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,17 @@
1
+ module Screenbeacon
2
+ module APIOperations
3
+ module Update
4
+ def save(params={})
5
+ values = self.class.serialize_params(self).merge(params)
6
+
7
+ if values.length > 0
8
+ values.delete(:id)
9
+
10
+ response, opts = request(:put, url, values)
11
+ refresh_from(response, opts)
12
+ end
13
+ self
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,35 @@
1
+ module Screenbeacon
2
+ class APIResource < ScreenbeaconObject
3
+ include Screenbeacon::APIOperations::Request
4
+
5
+ def self.class_name
6
+ self.name.split('::')[-1]
7
+ end
8
+
9
+ def self.url
10
+ if self == APIResource
11
+ raise NotImplementedError.new('APIResource is an abstract class. You should perform actions on its subclasses (Project, Test, etc.)')
12
+ end
13
+ "/#{CGI.escape(class_name.downcase)}s"
14
+ end
15
+
16
+ def url
17
+ unless id = self['id']
18
+ raise InvalidRequestError.new("Could not determine which URL to request: #{self.class} instance has invalid ID: #{id.inspect}", 'id')
19
+ end
20
+ "#{self.class.url}/#{CGI.escape(id.to_s)}"
21
+ end
22
+
23
+ def refresh
24
+ response, opts = request(:get, url, @retrieve_params)
25
+ refresh_from(response, opts)
26
+ end
27
+
28
+ def self.retrieve(id, opts={})
29
+ opts = Util.normalize_opts(opts)
30
+ instance = self.new(id, opts)
31
+ instance.refresh
32
+ instance
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,4 @@
1
+ module Screenbeacon
2
+ class APIConnectionError < ScreenbeaconError
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Screenbeacon
2
+ class APIError < ScreenbeaconError
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Screenbeacon
2
+ class AuthenticationError < ScreenbeaconError
3
+ end
4
+ end
@@ -0,0 +1,10 @@
1
+ module Screenbeacon
2
+ class InvalidRequestError < ScreenbeaconError
3
+ attr_accessor :param
4
+
5
+ def initialize(message, param, http_status=nil, http_body=nil, json_body=nil)
6
+ super(message, http_status, http_body, json_body)
7
+ @param = param
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,20 @@
1
+ module Screenbeacon
2
+ class ScreenbeaconError < StandardError
3
+ attr_reader :message
4
+ attr_reader :http_status
5
+ attr_reader :http_body
6
+ attr_reader :json_body
7
+
8
+ def initialize(message=nil, http_status=nil, http_body=nil, json_body=nil)
9
+ @message = message
10
+ @http_status = http_status
11
+ @http_body = http_body
12
+ @json_body = json_body
13
+ end
14
+
15
+ def to_s
16
+ status_string = @http_status.nil? ? "" : "(Status #{@http_status}) "
17
+ "#{status_string}#{@message}"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ module Screenbeacon
2
+ class Project < APIResource
3
+ include Screenbeacon::APIOperations::Create
4
+ include Screenbeacon::APIOperations::Update
5
+ include Screenbeacon::APIOperations::Delete
6
+ include Screenbeacon::APIOperations::List
7
+
8
+
9
+ def alerts
10
+ Alert.all({ :project_id => id }, @opts)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,263 @@
1
+ module Screenbeacon
2
+ class ScreenbeaconObject
3
+ include Enumerable
4
+
5
+ @@permanent_attributes = Set.new([:id])
6
+
7
+ # The default :id method is deprecated and isn't useful to us
8
+ if method_defined?(:id)
9
+ undef :id
10
+ end
11
+
12
+ def initialize(id=nil, opts={})
13
+ # parameter overloading!
14
+ if id.kind_of?(Hash)
15
+ @retrieve_params = id.dup
16
+ @retrieve_params.delete(:id)
17
+ id = id[:id]
18
+ else
19
+ @retrieve_params = {}
20
+ end
21
+
22
+ @opts = opts
23
+ @values = {}
24
+ # This really belongs in APIResource, but not putting it there allows us
25
+ # to have a unified inspect method
26
+ @unsaved_values = Set.new
27
+ @transient_values = Set.new
28
+ @values[:id] = id if id
29
+ end
30
+
31
+ def self.construct_from(values, opts={})
32
+ self.new(values[:id]).refresh_from(values, opts)
33
+ end
34
+
35
+ def to_s(*args)
36
+ JSON.pretty_generate(@values)
37
+ end
38
+
39
+ def inspect
40
+ id_string = (self.respond_to?(:id) && !self.id.nil?) ? " id=#{self.id}" : ""
41
+ "#<#{self.class}:0x#{self.object_id.to_s(16)}#{id_string}> JSON: " + JSON.pretty_generate(@values)
42
+ end
43
+
44
+ def refresh_from(values, opts, partial=false)
45
+ @opts = opts
46
+ @original_values = Marshal.load(Marshal.dump(values)) # deep copy
47
+ removed = partial ? Set.new : Set.new(@values.keys - values.keys)
48
+ added = Set.new(values.keys - @values.keys)
49
+ # Wipe old state before setting new. This is useful for e.g. updating a
50
+ # customer, where there is no persistent card parameter. Mark those values
51
+ # which don't persist as transient
52
+
53
+ instance_eval do
54
+ remove_accessors(removed)
55
+ add_accessors(added)
56
+ end
57
+ removed.each do |k|
58
+ @values.delete(k)
59
+ @transient_values.add(k)
60
+ @unsaved_values.delete(k)
61
+ end
62
+ values.each do |k, v|
63
+ @values[k] = Util.convert_to_screenbeacon_object(v, @opts)
64
+ @transient_values.delete(k)
65
+ @unsaved_values.delete(k)
66
+ end
67
+
68
+ return self
69
+ end
70
+
71
+ def [](k)
72
+ @values[k.to_sym]
73
+ end
74
+
75
+ def []=(k, v)
76
+ send(:"#{k}=", v)
77
+ end
78
+
79
+ def keys
80
+ @values.keys
81
+ end
82
+
83
+ def values
84
+ @values.values
85
+ end
86
+
87
+ def to_json(*a)
88
+ JSON.generate(@values)
89
+ end
90
+
91
+ def as_json(*a)
92
+ @values.as_json(*a)
93
+ end
94
+
95
+ def to_hash
96
+ @values.inject({}) do |acc, (key, value)|
97
+ acc[key] = value.respond_to?(:to_hash) ? value.to_hash : value
98
+ acc
99
+ end
100
+ end
101
+
102
+ def each(&blk)
103
+ @values.each(&blk)
104
+ end
105
+
106
+ def _dump(level)
107
+ Marshal.dump([@values, @opts])
108
+ end
109
+
110
+ def self._load(args)
111
+ values, opts = Marshal.load(args)
112
+ construct_from(values, opts)
113
+ end
114
+
115
+ if RUBY_VERSION < '1.9.2'
116
+ def respond_to?(symbol)
117
+ @values.has_key?(symbol) || super
118
+ end
119
+ end
120
+
121
+ def serialize_nested_object(key)
122
+ new_value = @values[key]
123
+ if new_value.is_a?(APIResource)
124
+ return {}
125
+ end
126
+
127
+ if @unsaved_values.include?(key)
128
+ # the object has been reassigned
129
+ # e.g. as object.key = {foo => bar}
130
+ update = new_value
131
+ new_keys = update.keys.map(&:to_sym)
132
+
133
+ # remove keys at the server, but not known locally
134
+ if @original_values.include?(key)
135
+ keys_to_unset = @original_values[key].keys - new_keys
136
+ keys_to_unset.each {|key| update[key] = ''}
137
+ end
138
+
139
+ update
140
+ else
141
+ # can be serialized normally
142
+ self.class.serialize_params(new_value)
143
+ end
144
+ end
145
+
146
+ def self.serialize_params(obj, original_value=nil)
147
+ case obj
148
+ when nil
149
+ ''
150
+ when ScreenbeaconObject
151
+ unsaved_keys = obj.instance_variable_get(:@unsaved_values)
152
+ obj_values = obj.instance_variable_get(:@values)
153
+ update_hash = {}
154
+
155
+ unsaved_keys.each do |k|
156
+ update_hash[k] = serialize_params(obj_values[k])
157
+ end
158
+
159
+ obj_values.each do |k, v|
160
+ if v.is_a?(ScreenbeaconObject) || v.is_a?(Hash)
161
+ update_hash[k] = obj.serialize_nested_object(k)
162
+ elsif v.is_a?(Array)
163
+ original_value = obj.instance_variable_get(:@original_values)[k]
164
+ if original_value && original_value.length > v.length
165
+ # url params provide no mechanism for deleting an item in an array,
166
+ # just overwriting the whole array or adding new items. So let's not
167
+ # allow deleting without a full overwrite until we have a solution.
168
+ raise ArgumentError.new(
169
+ "You cannot delete an item from an array, you must instead set a new array"
170
+ )
171
+ end
172
+ update_hash[k] = serialize_params(v, original_value)
173
+ end
174
+ end
175
+
176
+ update_hash
177
+ when Array
178
+ update_hash = {}
179
+ obj.each_with_index do |value, index|
180
+ update = serialize_params(value)
181
+ if update != {} && (!original_value || update != original_value[index])
182
+ update_hash[index] = update
183
+ end
184
+ end
185
+
186
+ if update_hash == {}
187
+ nil
188
+ else
189
+ update_hash
190
+ end
191
+ else
192
+ obj
193
+ end
194
+ end
195
+
196
+ protected
197
+
198
+ def metaclass
199
+ class << self; self; end
200
+ end
201
+
202
+ def remove_accessors(keys)
203
+ metaclass.instance_eval do
204
+ keys.each do |k|
205
+ next if @@permanent_attributes.include?(k)
206
+ k_eq = :"#{k}="
207
+ remove_method(k) if method_defined?(k)
208
+ remove_method(k_eq) if method_defined?(k_eq)
209
+ end
210
+ end
211
+ end
212
+
213
+ def add_accessors(keys)
214
+ metaclass.instance_eval do
215
+ keys.each do |k|
216
+ next if @@permanent_attributes.include?(k)
217
+ k_eq = :"#{k}="
218
+ define_method(k) { @values[k] }
219
+ define_method(k_eq) do |v|
220
+ if v == ""
221
+ raise ArgumentError.new(
222
+ "You cannot set #{k} to an empty string." \
223
+ "We interpret empty strings as nil in requests." \
224
+ "You may set #{self}.#{k} = nil to delete the property.")
225
+ end
226
+ @values[k] = v
227
+ @unsaved_values.add(k)
228
+ end
229
+ end
230
+ end
231
+ end
232
+
233
+ def method_missing(name, *args)
234
+ # TODO: only allow setting in updateable classes.
235
+ if name.to_s.end_with?('=')
236
+ attr = name.to_s[0...-1].to_sym
237
+ add_accessors([attr])
238
+ begin
239
+ mth = method(name)
240
+ rescue NameError
241
+ raise NoMethodError.new("Cannot set #{attr} on this object. HINT: you can't set: #{@@permanent_attributes.to_a.join(', ')}")
242
+ end
243
+ return mth.call(args[0])
244
+ else
245
+ return @values[name] if @values.has_key?(name)
246
+ end
247
+
248
+ begin
249
+ super
250
+ rescue NoMethodError => e
251
+ if @transient_values.include?(name)
252
+ raise NoMethodError.new(e.message + ". HINT: The '#{name}' attribute was set in the past, however. It was then wiped when refreshing the object with the result returned by Screenbeacon's API, probably as a result of a save(). The attributes currently available on this object are: #{@values.keys.join(', ')}")
253
+ else
254
+ raise
255
+ end
256
+ end
257
+ end
258
+
259
+ def respond_to_missing?(symbol, include_private = false)
260
+ @values && @values.has_key?(symbol) || super
261
+ end
262
+ end
263
+ end
@@ -0,0 +1,8 @@
1
+ module Screenbeacon
2
+ class Test < APIResource
3
+ include Screenbeacon::APIOperations::Create
4
+ include Screenbeacon::APIOperations::Update
5
+ include Screenbeacon::APIOperations::Delete
6
+ include Screenbeacon::APIOperations::List
7
+ end
8
+ end
@@ -0,0 +1,130 @@
1
+ module Screenbeacon
2
+ module Util
3
+ def self.objects_to_ids(h)
4
+ case h
5
+ when APIResource
6
+ h.id
7
+ when Hash
8
+ res = {}
9
+ h.each { |k, v| res[k] = objects_to_ids(v) unless v.nil? }
10
+ res
11
+ when Array
12
+ h.map { |v| objects_to_ids(v) }
13
+ else
14
+ h
15
+ end
16
+ end
17
+
18
+ def self.object_classes
19
+ @object_classes ||= {
20
+ # data structures
21
+ 'project' => Project,
22
+ 'test' => Test,
23
+ 'alert' => Alert
24
+ }
25
+ end
26
+
27
+ def self.convert_to_screenbeacon_object(resp, opts)
28
+ case resp
29
+ when Array
30
+ resp.map { |i| convert_to_screenbeacon_object(i, opts) }
31
+ when Hash
32
+ # Try converting to a known object class. If none available, fall back to generic ScreenbeaconObject
33
+ object_classes.fetch(resp[:object], ScreenbeaconObject).construct_from(resp, opts)
34
+ else
35
+ resp
36
+ end
37
+ end
38
+
39
+ def self.file_readable(file)
40
+ # This is nominally equivalent to File.readable?, but that can
41
+ # report incorrect results on some more oddball filesystems
42
+ # (such as AFS)
43
+ begin
44
+ File.open(file) { |f| }
45
+ rescue
46
+ false
47
+ else
48
+ true
49
+ end
50
+ end
51
+
52
+ def self.symbolize_names(object)
53
+ case object
54
+ when Hash
55
+ new_hash = {}
56
+ object.each do |key, value|
57
+ key = (key.to_sym rescue key) || key
58
+ new_hash[key] = symbolize_names(value)
59
+ end
60
+ new_hash
61
+ when Array
62
+ object.map { |value| symbolize_names(value) }
63
+ else
64
+ object
65
+ end
66
+ end
67
+
68
+ def self.url_encode(key)
69
+ URI.escape(key.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
70
+ end
71
+
72
+ def self.flatten_params(params, parent_key=nil)
73
+ result = []
74
+ params.each do |key, value|
75
+ calculated_key = parent_key ? "#{parent_key}[#{url_encode(key)}]" : url_encode(key)
76
+ if value.is_a?(Hash)
77
+ result += flatten_params(value, calculated_key)
78
+ elsif value.is_a?(Array)
79
+ result += flatten_params_array(value, calculated_key)
80
+ else
81
+ result << [calculated_key, value]
82
+ end
83
+ end
84
+ result
85
+ end
86
+
87
+ def self.flatten_params_array(value, calculated_key)
88
+ result = []
89
+ value.each do |elem|
90
+ if elem.is_a?(Hash)
91
+ result += flatten_params(elem, calculated_key)
92
+ elsif elem.is_a?(Array)
93
+ result += flatten_params_array(elem, calculated_key)
94
+ else
95
+ result << ["#{calculated_key}[]", elem]
96
+ end
97
+ end
98
+ result
99
+ end
100
+
101
+ # The secondary opts argument can either be a string or hash
102
+ # Turn this value into an api_key and a set of headers
103
+ def self.normalize_opts(opts)
104
+ case opts
105
+ when String
106
+ {:api_key => opts}
107
+ when Hash
108
+ check_api_id!(opts.fetch(:api_id)) if opts.has_key?(:api_id)
109
+ check_api_token!(opts.fetch(:api_token)) if opts.has_key?(:api_token)
110
+ opts.clone
111
+ else
112
+ raise TypeError.new('normalize_opts expects a string or a hash')
113
+ end
114
+ end
115
+
116
+ def self.check_string_argument!(key)
117
+ raise TypeError.new("argument must be a string") unless key.is_a?(String)
118
+ key
119
+ end
120
+
121
+ def self.check_api_id!(id)
122
+ raise TypeError.new("api_id must be a string") unless id.is_a?(String)
123
+ id
124
+ end
125
+ def self.check_api_token!(token)
126
+ raise TypeError.new("api_token must be a string") unless token.is_a?(String)
127
+ token
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,3 @@
1
+ module Screenbeacon
2
+ VERSION = '0.1.0'
3
+ end