zenaton 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.
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'httparty'
4
+ require 'zenaton/exceptions'
5
+
6
+ module Zenaton
7
+ # Collection of utility classes for the Zenaton library
8
+ module Services
9
+ # Wrapper class around HTTParty that:
10
+ # - handles http calls
11
+ # - sets appropriate headers for each request type
12
+ # - translates exceptions into Zenaton specific ones
13
+ class Http
14
+ # Makes a GET request and sets the correct headers
15
+ #
16
+ # @param url [String] the url for the request
17
+ # @return [Hash] the parsed json response
18
+ def get(url)
19
+ request(:get, url, default_options)
20
+ end
21
+
22
+ # Makes a POST request with some data and sets the correct headers
23
+ #
24
+ # @param url [String] the url for the request
25
+ # @param body [Hash] the payload to send with the request
26
+ # @return [Hash] the parsed json response
27
+ def post(url, body)
28
+ request(:post, url, post_options(body))
29
+ end
30
+
31
+ # Makes a PUT request with some data and sets the correct headers
32
+ #
33
+ # @param url [String] the url for the request
34
+ # @param body [Hash] the payload to send with the request
35
+ # @return [Hash] the parsed json response
36
+ def put(url, body)
37
+ request(:put, url, put_options(body))
38
+ end
39
+
40
+ private
41
+
42
+ def request(verb, url, options)
43
+ make_request(verb, url, options)
44
+ rescue SocketError, HTTParty::Error => error
45
+ raise Zenaton::ConnectionError, error
46
+ end
47
+
48
+ def make_request(verb, url, options)
49
+ response = HTTParty.send(verb, url, options)
50
+ raise Zenaton::InternalError, format_error(response) if errors? response
51
+ JSON.parse(response.body)
52
+ end
53
+
54
+ def errors?(response)
55
+ response.code >= 400
56
+ end
57
+
58
+ def format_error(response)
59
+ "#{response.code}: #{response.message}"
60
+ end
61
+
62
+ def default_options
63
+ {
64
+ headers: { 'Accept' => 'application/json' }
65
+ }
66
+ end
67
+
68
+ def post_options(body)
69
+ {
70
+ body: body.to_json,
71
+ headers: {
72
+ 'Accept' => 'application/json',
73
+ 'Content-Type' => 'application/json'
74
+ }
75
+ }
76
+ end
77
+ alias put_options post_options
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+
5
+ module Zenaton
6
+ module Services
7
+ # Wrapper class to read instance variables from an object and
8
+ # to create new objects with a given set of instance variables.
9
+ class Properties
10
+ # Handle (de)serializaton separately for these classes.
11
+ SPECIAL_CASES = [Time, Date, DateTime].freeze
12
+
13
+ # Returns an allocated instance of the given class name
14
+ # @param class_name [String] the name of the class to allocate
15
+ # @return [Object]
16
+ def blank_instance(class_name)
17
+ klass = Object.const_get(class_name)
18
+ if klass < Singleton
19
+ klass.instance
20
+ else
21
+ klass.allocate
22
+ end
23
+ end
24
+
25
+ # Returns a hash with the instance variables of a given object
26
+ # @param object [Object] the object to be read
27
+ # @return [Hash]
28
+ def from(object)
29
+ return from_complex_type(object) if special_case?(object)
30
+ object.instance_variables.map do |ivar|
31
+ value = object.instance_variable_get(ivar)
32
+ [ivar, value]
33
+ end.to_h
34
+ end
35
+
36
+ # Returns the given object with the properties as instance variables
37
+ # @param object [Object] the object to write the variables to
38
+ # @param properties [Hash] the properties to be written
39
+ # @return [Object]
40
+ def set(object, properties)
41
+ return set_complex_type(object, properties) if special_case?(object)
42
+ properties.each do |ivar, value|
43
+ object.instance_variable_set(ivar, value)
44
+ end
45
+ object
46
+ end
47
+
48
+ # Given a class name and a set of properties, return a new instance of the
49
+ # class with the given properties as instance variables
50
+ # @param class_name [String] name of the class to instantiate
51
+ # @param properties [Hash] the properties to be written
52
+ # @param super_class [Class] the optional class the object should inherit
53
+ # @return [Object]
54
+ def object_from(class_name, properties, super_class = nil)
55
+ blank_instance(class_name).tap do |object|
56
+ check_class(object, super_class)
57
+ set(object, properties)
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ def check_class(object, super_class)
64
+ msg = "Error - #{object.class} should be an instance of #{super_class}"
65
+ raise ArgumentError, msg unless valid_object(object, super_class)
66
+ end
67
+
68
+ def valid_object(object, super_class)
69
+ super_class.nil? || object.is_a?(super_class)
70
+ end
71
+
72
+ def from_complex_type(object)
73
+ case object.class.name
74
+ when 'Time'
75
+ from_time(object)
76
+ when 'Date'
77
+ from_date(object)
78
+ when 'DateTime'
79
+ from_date_time(object)
80
+ end
81
+ end
82
+
83
+ def from_time(object)
84
+ nanoseconds = [object.tv_usec * 1_000]
85
+ object.respond_to?(:tv_nsec) && nanoseconds << object.tv_nsec
86
+ { 's' => object.tv_sec, 'n' => nanoseconds.max }
87
+ end
88
+
89
+ def from_date(object)
90
+ { 'y' => object.year, 'm' => object.month,
91
+ 'd' => object.day, 'sg' => object.start }
92
+ end
93
+
94
+ def from_date_time(object)
95
+ {
96
+ 'y' => object.year, 'm' => object.month, 'd' => object.day,
97
+ 'H' => object.hour, 'M' => object.minute, 'S' => object.sec,
98
+ 'of' => object.offset.to_s, 'sg' => object.start
99
+ }
100
+ end
101
+
102
+ def set_complex_type(object, props)
103
+ case object.class.name
104
+ when 'Time'
105
+ return_time(object, props)
106
+ when 'Date'
107
+ return_date(props)
108
+ when 'DateTime'
109
+ return_date_time(props)
110
+ end
111
+ end
112
+
113
+ def return_time(object, props)
114
+ if object.respond_to?(:tv_usec)
115
+ Time.at(props['s'], Rational(props['n'], 1000))
116
+ else
117
+ Time.at(props['s'], props['n'] / 1000)
118
+ end
119
+ end
120
+
121
+ def return_date(props)
122
+ Date.civil(*props.values_at('y', 'm', 'd', 'sg'))
123
+ end
124
+
125
+ def return_date_time(props)
126
+ args = props.values_at('y', 'm', 'd', 'H', 'M', 'S')
127
+ of_a, of_b = props['of'].split('/')
128
+ args << if of_b && of_b != 0
129
+ Rational(of_a.to_i, of_b.to_i)
130
+ else
131
+ of_a
132
+ end
133
+ args << props['sg']
134
+ DateTime.civil(*args) # rubocop:disable Style/DateTime
135
+ end
136
+
137
+ def special_case?(object)
138
+ SPECIAL_CASES.include?(object.class)
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zenaton/services/properties'
4
+
5
+ module Zenaton
6
+ module Services
7
+ # Encoding and decoding ruby objects into Zenaton's json format
8
+ class Serializer
9
+ # this string prefixs ids that are used to identify objects
10
+ ID_PREFIX = '@zenaton#'
11
+
12
+ KEY_OBJECT = 'o' # JSON key for objects
13
+ KEY_OBJECT_NAME = 'n' # JSON key for class name
14
+ KEY_OBJECT_PROPERTIES = 'p' # JSON key for object ivars
15
+ KEY_ARRAY = 'a' # JSON key for array and hashes
16
+ KEY_DATA = 'd' # JSON key for json compatibles types
17
+ KEY_STORE = 's' # JSON key for deserialized complex object
18
+
19
+ def initialize
20
+ @properties = Properties.new
21
+ end
22
+
23
+ # rubocop:disable Metrics/MethodLength
24
+
25
+ # Encodes a given ruby object to Zenaton's json format
26
+ def encode(data)
27
+ @encoded = []
28
+ @decoded = []
29
+ value = {}
30
+ raise ArgumentError, 'Procs cannot be serialized' if data.is_a?(Proc)
31
+ if data.is_a?(Array)
32
+ value[KEY_ARRAY] = encode_array(data)
33
+ elsif data.is_a?(Hash)
34
+ value[KEY_ARRAY] = encode_hash(data)
35
+ elsif basic_type?(data)
36
+ value[KEY_DATA] = data
37
+ else
38
+ value[KEY_OBJECT] = encode_object(data)
39
+ end
40
+ value[KEY_STORE] = @encoded
41
+ value.to_json
42
+ end
43
+
44
+ # Decodes Zenaton's format in a valid Ruby object
45
+ def decode(json_string)
46
+ parsed_json = JSON.parse(json_string)
47
+ @decoded = []
48
+ @encoded = parsed_json.delete(KEY_STORE)
49
+ case parsed_json.keys.first
50
+ when KEY_DATA
51
+ return parsed_json[KEY_DATA]
52
+ when KEY_ARRAY
53
+ return decode_enumerable(parsed_json[KEY_ARRAY])
54
+ when KEY_OBJECT
55
+ id = parsed_json[KEY_OBJECT][ID_PREFIX.length..-1].to_i
56
+ return decode_object(id, @encoded[id])
57
+ end
58
+ end
59
+ # rubocop:enable Metrics/MethodLength
60
+
61
+ private
62
+
63
+ def array_type?(data)
64
+ data.is_a?(Array) || data.is_a?(Hash)
65
+ end
66
+
67
+ def basic_type?(data)
68
+ data.is_a?(String) \
69
+ || data.is_a?(Integer) \
70
+ || data.is_a?(Float) \
71
+ || data == true \
72
+ || data == false \
73
+ || data.nil?
74
+ end
75
+
76
+ def encode_array(array)
77
+ array.map { |elem| encode_value(elem) }
78
+ end
79
+
80
+ def encode_hash(hash)
81
+ hash.transform_values { |value| encode_value(value) }
82
+ end
83
+
84
+ def encode_value(value)
85
+ raise ArgumentError, 'Procs cannot be serialized' if value.is_a?(Proc)
86
+ if value.is_a?(Array)
87
+ encode_array(value)
88
+ elsif value.is_a?(Hash)
89
+ encode_hash(value)
90
+ elsif basic_type?(value)
91
+ value
92
+ else
93
+ encode_object(value)
94
+ end
95
+ end
96
+
97
+ def encode_object(object)
98
+ id = @decoded.index(object)
99
+ unless id
100
+ id = @decoded.length
101
+ @decoded[id] = object
102
+ @encoded[id] = {
103
+ KEY_OBJECT_NAME => object.class.name,
104
+ KEY_OBJECT_PROPERTIES => encode_hash(@properties.from(object))
105
+ }
106
+ end
107
+ "#{ID_PREFIX}#{id}"
108
+ end
109
+
110
+ def object_id?(string)
111
+ string.is_a?(String) \
112
+ && string.start_with?(ID_PREFIX) \
113
+ && string[ID_PREFIX.length..-1].to_i <= @encoded.length
114
+ end
115
+
116
+ def decode_enumerable(enumerable)
117
+ return decode_array(enumerable) if enumerable.is_a?(Array)
118
+ return decode_hash(enumerable) if enumerable.is_a?(Hash)
119
+ raise ArgumentError, 'Unknown type'
120
+ end
121
+
122
+ def decode_array(array)
123
+ array.map { |elem| decode_element(elem) }
124
+ end
125
+
126
+ def decode_hash(hash)
127
+ hash.transform_values { |value| decode_element(value) }
128
+ end
129
+
130
+ # rubocop:disable Metrics/MethodLength
131
+ def decode_element(value)
132
+ if object_id?(value)
133
+ id = value[ID_PREFIX.length..-1].to_i
134
+ encoded = @encoded[id]
135
+ decode_object(id, encoded) if encoded.is_a?(Hash)
136
+ elsif value.is_a?(Array)
137
+ decode_array(value)
138
+ elsif value.is_a?(Hash)
139
+ decode_hash(value)
140
+ else
141
+ value
142
+ end
143
+ end
144
+ # rubocop:enable Metrics/MethodLength
145
+
146
+ def decode_object(id, encoded_object)
147
+ decoded = @decoded[id]
148
+ return decoded if decoded
149
+ object = @properties.blank_instance(encoded_object[KEY_OBJECT_NAME])
150
+ @decoded[id] = object
151
+ properties = decode_hash(encoded_object[KEY_OBJECT_PROPERTIES])
152
+ @properties.set(object, properties)
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zenaton/exceptions'
4
+ require 'zenaton/interfaces/task'
5
+ require 'zenaton/interfaces/event'
6
+ require 'zenaton/traits/with_timestamp'
7
+ require 'zenaton/traits/zenatonable'
8
+
9
+ module Zenaton
10
+ # Subclasses of Zenaton::Interfaces::Task
11
+ module Tasks
12
+ # Class for creating waiting tasks
13
+ class Wait < Interfaces::Task
14
+ attr_reader :event
15
+
16
+ include Traits::WithTimestamp
17
+ include Traits::Zenatonable
18
+
19
+ # Creates a new wait task and validates the event given
20
+ # @param event [Class, String]
21
+ def initialize(event = nil)
22
+ raise ExternalError, error unless valid_param(event)
23
+ @event = event
24
+ end
25
+
26
+ # NOOP: No waiting when executing locally
27
+ def handle; end
28
+
29
+ private
30
+
31
+ def error
32
+ # rubocop:disable Metrics/LineLength
33
+ "#{self.class}: Invalid parameter - argument must be a Zenaton::Interfaces::Event subclass"
34
+ # rubocop:enable Metrics/LineLength
35
+ end
36
+
37
+ def valid_param(event)
38
+ event.nil? || event.is_a?(String) || event_class?(event)
39
+ end
40
+
41
+ def event_class?(event)
42
+ event.class == Class && event < Zenaton::Interfaces::Event
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/time'
4
+
5
+ module Zenaton
6
+ module Traits
7
+ # Module to calculate duration between events
8
+ module WithDuration
9
+ # @return [Integer, NilClass] Duration in seconds
10
+ def _get_duration
11
+ return unless @buffer
12
+ now, now_dup = _init_now_then
13
+ @buffer.each do |time_unit, time_value|
14
+ now_dup = _apply_duration(time_unit, time_value, now_dup)
15
+ end
16
+ diff_in_seconds(now, now_dup)
17
+ end
18
+
19
+ %i[seconds minutes hours days weeks months years].each do |method_name|
20
+ define_method method_name do |value = 1|
21
+ _push(method_name, value)
22
+ self
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def _init_now_then
29
+ Time.zone = self.class.class_variable_get(:@@_timezone) || 'UTC'
30
+ now = Time.zone.now
31
+ Time.zone = nil # Resets time zone
32
+ [now, now.dup]
33
+ end
34
+
35
+ def _push(method_name, value)
36
+ @buffer ||= {}
37
+ @buffer[method_name] = value
38
+ end
39
+
40
+ def _apply_duration(time_unit, time_value, time)
41
+ time + time_value.send(time_unit)
42
+ end
43
+
44
+ def diff_in_seconds(before, after)
45
+ (after - before).to_i
46
+ end
47
+ end
48
+ end
49
+ end