zenaton 0.1.0

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