screenbeacon 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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