omnievent 0.1.0.pre1

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.
@@ -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