omnievent 0.1.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,290 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniEvent
4
+ # The EventHash is a normalized schema returned by all OmniEvent strategies.
5
+ class EventHash < OmniEvent::KeyStore
6
+ def self.subkey_class
7
+ Hashie::Mash
8
+ end
9
+
10
+ # Tells you if this is considered to be a valid EventHash.
11
+ def valid?
12
+ provider? &&
13
+ data? &&
14
+ data.valid? &&
15
+ (!metadata || metadata.valid?) &&
16
+ (!associated_data || associated_data.valid?)
17
+ end
18
+
19
+ def regular_writer(key, value)
20
+ value = DataHash.new(value) if key.to_s == "data" && value.is_a?(::Hash) && !value.is_a?(DataHash)
21
+ value = MetadataHash.new(value) if key.to_s == "metadata" && value.is_a?(::Hash) && !value.is_a?(MetadataHash)
22
+ if key.to_s == "associated_data" && value.is_a?(::Hash) && !value.is_a?(AssociatedDataHash)
23
+ value = AssociatedDataHash.new(value)
24
+ end
25
+ super
26
+ end
27
+
28
+ # Base for data hashes
29
+ class EventHashBase < OmniEvent::KeyStore
30
+ def permitted?(keys, attribute = nil, sub_attribute = nil)
31
+ permitted = self.class.permitted_attributes
32
+ permitted = permitted[attribute] if attribute
33
+ permitted = permitted[sub_attribute] if sub_attribute
34
+ permitted = permitted.is_a?(Hash) ? permitted.keys.map(&:to_s) : permitted
35
+ (keys - permitted).empty?
36
+ end
37
+
38
+ # Tells you if this is considered to be a valid hash.
39
+ def valid?
40
+ if self.class.respond_to?(:required_attributes) && !self.class.required_attributes.all? do |attribute|
41
+ value = send(attribute)
42
+ !value.nil? && !value.empty?
43
+ end
44
+ # All required attributes have values
45
+ return false
46
+ end
47
+
48
+ # All attributes are permitted
49
+ return false unless permitted?(keys)
50
+
51
+ # All attribute values are valid
52
+ self.class.permitted_attributes.all? do |attribute|
53
+ value = send(attribute.to_s)
54
+ return true if value.to_s.empty? || send("#{attribute}_valid?")
55
+
56
+ invalid << attribute
57
+ false
58
+ end
59
+ end
60
+
61
+ def invalid
62
+ @invalid ||= []
63
+ end
64
+ end
65
+
66
+ # The event data.
67
+ class DataHash < EventHashBase
68
+ def self.subkey_class
69
+ Hashie::Mash
70
+ end
71
+
72
+ def self.permitted_attributes
73
+ %w[
74
+ start_time
75
+ end_time
76
+ name
77
+ description
78
+ url
79
+ ]
80
+ end
81
+
82
+ def self.required_attributes
83
+ %w[
84
+ start_time
85
+ name
86
+ ]
87
+ end
88
+
89
+ def start_time_valid?
90
+ OmniEvent::Utils.valid_time?(start_time)
91
+ end
92
+
93
+ def end_time_valid?
94
+ OmniEvent::Utils.valid_time?(end_time)
95
+ end
96
+
97
+ def name_valid?
98
+ OmniEvent::Utils.valid_type?(name, :string)
99
+ end
100
+
101
+ def description_valid?
102
+ OmniEvent::Utils.valid_type?(name, :string)
103
+ end
104
+
105
+ def url_valid?
106
+ OmniEvent::Utils.valid_url?(url)
107
+ end
108
+ end
109
+
110
+ # The event metadata.
111
+ class MetadataHash < EventHashBase
112
+ def self.subkey_class
113
+ Hashie::Mash
114
+ end
115
+
116
+ # The permitted MetadataHash attributes.
117
+ def self.permitted_attributes
118
+ %w[
119
+ uid
120
+ created_at
121
+ updated_at
122
+ language
123
+ status
124
+ taxonomies
125
+ ]
126
+ end
127
+
128
+ def self.permitted_statuses
129
+ %w[
130
+ draft
131
+ published
132
+ cancelled
133
+ ]
134
+ end
135
+
136
+ def uid_valid?
137
+ OmniEvent::Utils.valid_uid?(uid)
138
+ end
139
+
140
+ def created_at_valid?
141
+ OmniEvent::Utils.valid_time?(created_at)
142
+ end
143
+
144
+ def updated_at_valid?
145
+ OmniEvent::Utils.valid_time?(updated_at)
146
+ end
147
+
148
+ def language_valid?
149
+ OmniEvent::Utils.valid_language_code?(language)
150
+ end
151
+
152
+ def status_valid?
153
+ self.class.permitted_statuses.include?(status)
154
+ end
155
+
156
+ def taxonomies_valid?
157
+ OmniEvent::Utils.all_valid_type?(taxonomies, :string)
158
+ end
159
+ end
160
+
161
+ # The event's associated data.
162
+ class AssociatedDataHash < EventHashBase
163
+ def self.subkey_class
164
+ Hashie::Mash
165
+ end
166
+
167
+ # The permitted MetadataHash attributes.
168
+ def self.permitted_attributes
169
+ {
170
+ location: %w[uid name address city postal_code country latitude longitude url],
171
+ virtual_location: { "uid": "", "entry_points": %w[uri type code label] },
172
+ organizer: %w[uid name email uris],
173
+ registrations: %w[uid name email status]
174
+ }
175
+ end
176
+
177
+ def self.permitted_entry_point_attributes
178
+ %w[
179
+ uri
180
+ type
181
+ code
182
+ label
183
+ ]
184
+ end
185
+
186
+ def location_valid?
187
+ return true unless location
188
+ return false unless location.is_a?(Hash)
189
+ return false unless permitted?(location.keys, :location)
190
+
191
+ location.all? do |key, value|
192
+ case key
193
+ when "uid"
194
+ OmniEvent::Utils.valid_uid?(value)
195
+ when "name", "address", "city", "postal_code"
196
+ OmniEvent::Utils.valid_type?(value, :string)
197
+ when "country"
198
+ OmniEvent::Utils.valid_country_code?(value)
199
+ when "latitude", "longitude"
200
+ OmniEvent::Utils.valid_coordinate?(value, key.to_sym)
201
+ when "url"
202
+ OmniEvent::Utils.valid_url?(value)
203
+ else
204
+ false
205
+ end
206
+ end
207
+ end
208
+
209
+ def virtual_location_valid?
210
+ return true unless virtual_location
211
+ return false unless virtual_location.is_a?(Hash)
212
+ return true unless virtual_location["entry_points"]
213
+
214
+ return false if virtual_location["uid"] && !OmniEvent::Utils.valid_uid?(virtual_location["uid"])
215
+
216
+ virtual_location["entry_points"].all? do |entry_point|
217
+ return false unless entry_point.is_a?(Hash)
218
+ return false unless entry_point["uri"] && entry_point["type"]
219
+ return false unless permitted?(entry_point.keys, :virtual_location, :entry_points)
220
+ return false unless case entry_point["type"]
221
+ when "video"
222
+ OmniEvent::Utils.valid_url?(entry_point["uri"])
223
+ when "phone", "sip"
224
+ OmniEvent::Utils.valid_type?(entry_point["uri"], :string)
225
+ else
226
+ false
227
+ end
228
+
229
+ OmniEvent::Utils.valid_type?(entry_point["label"], :string) &&
230
+ OmniEvent::Utils.valid_type?(entry_point["code"], :string)
231
+ end
232
+ end
233
+
234
+ def organizer_valid?
235
+ return true unless organizer
236
+ return false unless organizer.is_a?(Hash)
237
+ return false unless permitted?(organizer.keys, :organizer)
238
+
239
+ organizer.all? do |key, value|
240
+ case key
241
+ when "uid"
242
+ OmniEvent::Utils.valid_uid?(value)
243
+ when "name"
244
+ OmniEvent::Utils.valid_type?(value, :string)
245
+ when "email"
246
+ OmniEvent::Utils.valid_email?(value)
247
+ when "uris"
248
+ OmniEvent::Utils.all_valid_type?(value, :string)
249
+ else
250
+ false
251
+ end
252
+ end
253
+ end
254
+
255
+ def self.permitted_registration_statuses
256
+ %w[
257
+ confirmed
258
+ declined
259
+ tentative
260
+ ]
261
+ end
262
+
263
+ def registrations_valid?
264
+ return true unless registrations
265
+ return false unless registrations.is_a?(Array)
266
+
267
+ registrations.all? do |registration|
268
+ return false unless registration.is_a?(Hash)
269
+ return false unless registration["email"] && registration["status"]
270
+ return false unless permitted?(registration.keys, :registrations)
271
+
272
+ registration.all? do |key, value|
273
+ case key
274
+ when "uid"
275
+ OmniEvent::Utils.valid_uid?(value)
276
+ when "name"
277
+ OmniEvent::Utils.valid_type?(value, :string)
278
+ when "email"
279
+ OmniEvent::Utils.valid_email?(value)
280
+ when "status"
281
+ self.class.permitted_registration_statuses.include?(value)
282
+ else
283
+ false
284
+ end
285
+ end
286
+ end
287
+ end
288
+ end
289
+ end
290
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hashie/mash"
4
+
5
+ module OmniEvent
6
+ # Generic helper hash that allows method access on deeply nested keys.
7
+ class KeyStore < ::Hashie::Mash
8
+ # Disables warnings on Hashie 3.5.0+ for overwritten keys
9
+ def self.override_logging
10
+ require "hashie/version"
11
+ return unless Gem::Version.new(Hashie::VERSION) >= Gem::Version.new("3.5.0")
12
+
13
+ if respond_to?(:disable_warnings)
14
+ disable_warnings
15
+ else
16
+ define_method(:log_built_in_message) { |*| }
17
+ private :log_built_in_message
18
+ end
19
+ end
20
+
21
+ # Disable on loading of the class
22
+ override_logging
23
+ end
24
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module OmniEvent
6
+ module Strategies
7
+ # The Developer strategy can be used for testing purposes.
8
+ #
9
+ # ## Usage
10
+ #
11
+ # All you need to do is add it in like any other strategy:
12
+ #
13
+ # @example Basic Usage
14
+ #
15
+ # OmniEvent::Builder.new do
16
+ # provider :developer
17
+ # end
18
+ #
19
+ class Developer
20
+ include OmniEvent::Strategy
21
+
22
+ option :name, "developer"
23
+
24
+ def self.raw_data
25
+ fixture = File.join(File.expand_path("../../..", __dir__), "spec", "fixtures", "list_events.json")
26
+ @raw_data ||= JSON.parse(File.open(fixture).read).to_h
27
+ end
28
+
29
+ def raw_events
30
+ self.class.raw_data["events"]
31
+ end
32
+
33
+ def event_hash(raw_event)
34
+ event = OmniEvent::EventHash.new(
35
+ provider: name,
36
+ data: raw_event.slice(*OmniEvent::EventHash::DataHash.permitted_attributes),
37
+ metadata: raw_event.slice(*OmniEvent::EventHash::MetadataHash.permitted_attributes),
38
+ associated_data: {
39
+ location: map_location(raw_event["location"]),
40
+ virtual_location: raw_event["virtual_location"]
41
+ }
42
+ )
43
+
44
+ event.data.start_time = format_time(event.data.start_time)
45
+ event.data.end_time = format_time(event.data.end_time)
46
+ event.metadata.created_at = format_time(event.metadata.created_at)
47
+ event.metadata.updated_at = format_time(event.metadata.updated_at)
48
+ event.metadata.uid = raw_event["id"]
49
+
50
+ event
51
+ end
52
+
53
+ def authorized?
54
+ true
55
+ end
56
+
57
+ protected
58
+
59
+ def location_key_map
60
+ {
61
+ countryCode: "country",
62
+ latitude: "latitude",
63
+ longitude: "longitude",
64
+ address1: "address",
65
+ address2: "address",
66
+ address3: "address",
67
+ city: "city",
68
+ postalCode: "postal_code"
69
+ }
70
+ end
71
+
72
+ def map_location(raw_location)
73
+ raw_location.each_with_object({}) do |(raw_key, raw_value), result|
74
+ next unless location_key_map[raw_key.to_sym]
75
+
76
+ key = location_key_map[raw_key.to_sym]
77
+ value = result[key]
78
+ if value && key == "address"
79
+ value += " #{raw_value}"
80
+ else
81
+ value = raw_value
82
+ end
83
+ result[key] = value
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniEvent
4
+ # The Strategy is the base unit of OmniEvent's ability to handle
5
+ # multiple event providers. It's substantially based on OmniAuth::Strategy.
6
+ module Strategy
7
+ class NotImplementedError < NotImplementedError; end
8
+ class Options < OmniEvent::KeyStore; end
9
+
10
+ def self.included(base)
11
+ OmniEvent.strategies << base
12
+ base.extend ClassMethods
13
+ base.class_eval do
14
+ option :uid_delimiter, "-"
15
+ end
16
+ end
17
+
18
+ # Class methods for Strategy
19
+ module ClassMethods
20
+ # Default options for all strategies which can be overriden at the class-level
21
+ # for each strategy.
22
+ def default_options
23
+ @default_options ||= begin
24
+ d_opts = OmniEvent::Strategy::Options.new
25
+ d_opts.merge!(superclass.default_options) if superclass.respond_to?(:default_options)
26
+ d_opts
27
+ end
28
+ end
29
+
30
+ # This allows for more declarative subclassing of strategies by allowing
31
+ # default options to be set using a simple configure call.
32
+ #
33
+ # @param options [Hash] If supplied, these will be the default options
34
+ # (deep-merged into the superclass's default options).
35
+ # @yield [Options] The options Mash that allows you to set your defaults as you'd like.
36
+ #
37
+ # @example Using a yield to configure the default options.
38
+ #
39
+ # class MyStrategy
40
+ # include OmniEvent::Strategy
41
+ #
42
+ # configure do |c|
43
+ # c.foo = 'bar'
44
+ # end
45
+ # end
46
+ #
47
+ # @example Using a hash to configure the default options.
48
+ #
49
+ # class MyStrategy
50
+ # include OmniEvent::Strategy
51
+ # configure foo: 'bar'
52
+ # end
53
+ def configure(options = nil)
54
+ if block_given?
55
+ yield default_options
56
+ else
57
+ default_options.deep_merge!(options)
58
+ end
59
+ end
60
+
61
+ # Directly declare a default option for your class. This is a useful from
62
+ # a documentation perspective as it provides a simple line-by-line analysis
63
+ # of the kinds of options your strategy provides by default.
64
+ #
65
+ # @param name [Symbol] The key of the default option in your configuration hash.
66
+ # @param value [Object] The value your object defaults to. Nil if not provided.
67
+ #
68
+ # @example
69
+ #
70
+ # class MyStrategy
71
+ # include OmniEvent::Strategy
72
+ #
73
+ # option :foo, 'bar'
74
+ # option
75
+ # end
76
+ def option(name, value = nil)
77
+ default_options[name] = value
78
+ end
79
+
80
+ # Sets (and retrieves) option key names for initializer arguments to be
81
+ # recorded as. This takes care of 90% of the use cases for overriding
82
+ # the initializer in OmniEvent Strategies.
83
+ def args(args = nil)
84
+ if args
85
+ @args = Array(args)
86
+ return
87
+ end
88
+ existing = superclass.respond_to?(:args) ? superclass.args : []
89
+ (instance_variable_defined?(:@args) && @args) || existing
90
+ end
91
+ end
92
+
93
+ attr_reader :options
94
+
95
+ # Initializes the strategy. An `options` hash is automatically
96
+ # created from the last argument if it is a hash.
97
+ #
98
+ # @overload new(options = {})
99
+ # If nothing but a hash is supplied, initialized with the supplied options
100
+ # overriding the strategy's default options via a deep merge.
101
+ # @overload new(*args, options = {})
102
+ # If the strategy has supplied custom arguments that it accepts, they may
103
+ # will be passed through and set to the appropriate values.
104
+ #
105
+ # @yield [Options] Yields options to block for further configuration.
106
+ def initialize(*args, &block) # rubocop:disable Lint/UnusedMethodArgument
107
+ @options = self.class.default_options.dup
108
+
109
+ options.deep_merge!(args.pop) if args.last.is_a?(Hash)
110
+ options[:name] ||= self.class.to_s.split("::").last.downcase
111
+
112
+ self.class.args.each do |arg|
113
+ break if args.empty?
114
+
115
+ options[arg] = args.shift
116
+ end
117
+
118
+ # Make sure that all of the args have been dealt with, otherwise error out.
119
+
120
+ yield options if block_given?
121
+
122
+ validate_options
123
+ end
124
+
125
+ def validate_options
126
+ # rubocop:disable Style/GuardClause
127
+ if options[:from_time] && !options[:from_time].respond_to?(:strftime)
128
+ raise ArgumentError, "from_time must be a valid ruby time object"
129
+ end
130
+ if options[:to_time] && !options[:to_time].respond_to?(:strftime)
131
+ raise ArgumentError, "to_time must be a valid ruby time object"
132
+ end
133
+ # rubocop:enable Style/GuardClause
134
+ end
135
+
136
+ def request(method, opts)
137
+ options.deep_merge!(opts)
138
+
139
+ authorize
140
+ return unless authorized?
141
+
142
+ send(method)
143
+ end
144
+
145
+ def authorize; end
146
+
147
+ def authorized?
148
+ raise NotImplementedError
149
+ end
150
+
151
+ def raw_events
152
+ raise NotImplementedError
153
+ end
154
+
155
+ def event_hash
156
+ raise NotImplementedError
157
+ end
158
+
159
+ def list_events
160
+ raw_events.each_with_object([]) do |raw_event, result|
161
+ event = event_hash(raw_event)
162
+
163
+ next unless event.valid?
164
+ next if options.from_time && Time.parse(event.data.start_time).utc < options.from_time.utc
165
+ next if options.to_time && Time.parse(event.data.start_time).utc > options.to_time.utc
166
+
167
+ result << event
168
+ end
169
+ end
170
+
171
+ # Direct access to the OmniEvent logger, automatically prefixed
172
+ # with this strategy's name.
173
+ #
174
+ # @example
175
+ # log :warn, 'This is a warning.'
176
+ def log(level, message)
177
+ OmniEvent.logger.send(level, "(#{name}) #{message}")
178
+ end
179
+
180
+ def name
181
+ options[:name]
182
+ end
183
+
184
+ def format_time(time)
185
+ return nil if time.nil? || !time.respond_to?(:to_s)
186
+
187
+ OmniEvent::Utils.convert_time_to_iso8601(time.to_s)
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+ require "tzinfo"
5
+ require "iso-639"
6
+ require "time"
7
+
8
+ module OmniEvent
9
+ # Utility methods
10
+ module Utils
11
+ module_function
12
+
13
+ def camelize(word, first_letter_in_uppercase = true)
14
+ return OmniEvent.config.camelizations[word.to_s] if OmniEvent.config.camelizations[word.to_s]
15
+
16
+ if first_letter_in_uppercase
17
+ word.to_s.gsub(%r{/(.?)}) do
18
+ "::#{Regexp.last_match[1].upcase}"
19
+ end.gsub(/(^|_)(.)/) { Regexp.last_match[2].upcase }
20
+ else
21
+ camelize(word).tap { |w| w[0] = w[0].downcase }
22
+ end
23
+ end
24
+
25
+ def valid_time?(value)
26
+ !!Time.iso8601(value)
27
+ rescue ArgumentError
28
+ false
29
+ end
30
+
31
+ def valid_language_code?(value)
32
+ !!ISO_639.find_by_code(value)
33
+ end
34
+
35
+ def valid_country_code?(value)
36
+ !!TZInfo::Country.all_codes.include?(value)
37
+ end
38
+
39
+ def valid_coordinate?(value, type)
40
+ case type
41
+ when :latitude
42
+ /^-?([1-8]?\d(?:\.\d{1,})?|90(?:\.0{1,6})?)$/ =~ value
43
+ when :longitude
44
+ /^-?((?:1[0-7]|[1-9])?\d(?:\.\d{1,})?|180(?:\.0{1,})?)$/ =~ value
45
+ else
46
+ false
47
+ end
48
+ end
49
+
50
+ def valid_email?(value)
51
+ URI::MailTo::EMAIL_REGEXP =~ value
52
+ end
53
+
54
+ def valid_url?(value)
55
+ (URI.parse value).is_a? URI::HTTP
56
+ rescue URI::InvalidURIError
57
+ false
58
+ end
59
+
60
+ def valid_type?(value, type)
61
+ case type
62
+ when :boolean
63
+ [true, false].include? value
64
+ when :string
65
+ value.is_a?(String)
66
+ when :array
67
+ value.is_a?(Array)
68
+ else
69
+ false
70
+ end
71
+ end
72
+
73
+ def valid_uid?(value)
74
+ value.is_a?(String)
75
+ end
76
+
77
+ def all_valid_type?(array, type)
78
+ valid_type?(array, :array) && array.all? { |t| valid_type?(t, type) }
79
+ end
80
+
81
+ def convert_time_to_iso8601(value)
82
+ Time.parse(value).iso8601
83
+ rescue ArgumentError
84
+ nil
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniEvent
4
+ VERSION = "0.1.0.pre1"
5
+ end