zenaton 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.circleci/config.yml +45 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +20 -0
- data/.yardopts +1 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +106 -0
- data/LICENSE.txt +21 -0
- data/README.md +115 -0
- data/Rakefile +8 -0
- data/bin/console +7 -0
- data/bin/setup +6 -0
- data/lib/zenaton.rb +12 -0
- data/lib/zenaton/client.rb +211 -0
- data/lib/zenaton/engine.rb +72 -0
- data/lib/zenaton/exceptions.rb +24 -0
- data/lib/zenaton/interfaces/event.rb +9 -0
- data/lib/zenaton/interfaces/job.rb +16 -0
- data/lib/zenaton/interfaces/task.rb +16 -0
- data/lib/zenaton/interfaces/workflow.rb +23 -0
- data/lib/zenaton/parallel.rb +24 -0
- data/lib/zenaton/processor.rb +11 -0
- data/lib/zenaton/query/builder.rb +69 -0
- data/lib/zenaton/services/http.rb +80 -0
- data/lib/zenaton/services/properties.rb +142 -0
- data/lib/zenaton/services/serializer.rb +156 -0
- data/lib/zenaton/tasks/wait.rb +46 -0
- data/lib/zenaton/traits/with_duration.rb +49 -0
- data/lib/zenaton/traits/with_timestamp.rb +131 -0
- data/lib/zenaton/traits/zenatonable.rb +36 -0
- data/lib/zenaton/version.rb +6 -0
- data/lib/zenaton/workflows/version.rb +61 -0
- data/zenaton.gemspec +39 -0
- metadata +260 -0
@@ -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
|