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