zenaton 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/bin/setup ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
data/lib/zenaton.rb ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zenaton/version'
4
+ require 'zenaton/exceptions'
5
+ require 'zenaton/client'
6
+ require 'zenaton/interfaces/event'
7
+ require 'zenaton/tasks/wait'
8
+ require 'zenaton/parallel'
9
+
10
+ # Top level namespace for the Zenaton ruby library
11
+ module Zenaton
12
+ end
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+ require 'zenaton/services/http'
5
+ require 'zenaton/services/properties'
6
+ require 'zenaton/services/serializer'
7
+ require 'zenaton/workflows/version'
8
+
9
+ module Zenaton
10
+ # Zenaton Client
11
+ class Client
12
+ include Singleton
13
+
14
+ ZENATON_API_URL = 'https://zenaton.com/api/v1' # Zenaton api url
15
+ ZENATON_WORKER_URL = 'http://localhost' # Default worker url
16
+ DEFAULT_WORKER_PORT = 4001 # Default worker port
17
+ WORKER_API_VERSION = 'v_newton' # Default worker api version
18
+
19
+ MAX_ID_SIZE = 256 # Limit on length of custom ids
20
+
21
+ APP_ENV = 'app_env' # Parameter name for the application environment
22
+ APP_ID = 'app_id' # Parameter name for the application ID
23
+ API_TOKEN = 'api_token' # Parameter name for the API token
24
+
25
+ ATTR_ID = 'custom_id' # Parameter name for custom ids
26
+ ATTR_NAME = 'name' # Parameter name for workflow names
27
+ ATTR_CANONICAL = 'canonical_name' # Parameter name for version name
28
+ ATTR_DATA = 'data' # Parameter name for json payload
29
+ ATTR_PROG = 'programming_language' # Parameter name for the language
30
+ ATTR_MODE = 'mode' # Parameter name for the worker update mode
31
+
32
+ PROG = 'Ruby' # The current programming language
33
+
34
+ EVENT_INPUT = 'event_input' # Parameter name for event data
35
+ EVENT_NAME = 'event_name' # Parameter name for event name
36
+
37
+ WORKFLOW_KILL = 'kill' # Worker update mode to stop a worker
38
+ WORKFLOW_PAUSE = 'pause' # Worker udpate mode to pause a worker
39
+ WORKFLOW_RUN = 'run' # Worker update mode to resume a worker
40
+
41
+ attr_writer :app_id, :api_token, :app_env
42
+
43
+ # Class method that sets the three tokens needed to interact with the API
44
+ # @param app_id [String] the ID of your Zenaton application
45
+ # @param api_token [String] your Zenaton account API token
46
+ # @param app_env [String] the environment (dev, staging, prod) to run under
47
+ # @return [Zenaton::Client] the instance of the client.
48
+ def self.init(app_id, api_token, app_env)
49
+ instance.tap do |client|
50
+ client.app_id = app_id
51
+ client.api_token = api_token
52
+ client.app_env = app_env
53
+ end
54
+ end
55
+
56
+ # @private
57
+ def initialize
58
+ @http = Services::Http.new
59
+ @serializer = Services::Serializer.new
60
+ @properties = Services::Properties.new
61
+ end
62
+
63
+ # Gets the url for the workers
64
+ # @param resource [String] the endpoint for the worker
65
+ # @param params [String] url encoded parameters to include in request
66
+ # @return [String] the workers url with parameters
67
+ def worker_url(resource = '', params = '')
68
+ base_url = ENV['ZENATON_WORKER_URL'] || ZENATON_WORKER_URL
69
+ port = ENV['ZENATON_WORKER_PORT'] || DEFAULT_WORKER_PORT
70
+ url = "#{base_url}:#{port}/api/#{WORKER_API_VERSION}/#{resource}?"
71
+ add_app_env(url, params)
72
+ end
73
+
74
+ # Gets the url for zenaton api
75
+ # @param resource [String] the endpoint for the api
76
+ # @param params [String] url encoded parameters to include in request
77
+ # @return [String] the api url with parameters
78
+ def website_url(resource = '', params = '')
79
+ api_url = ENV['ZENATON_API_URL'] || ZENATON_API_URL
80
+ url = "#{api_url}/#{resource}?#{API_TOKEN}=#{@api_token}&"
81
+ add_app_env(url, params)
82
+ end
83
+
84
+ # Start the specified workflow
85
+ # @param flow [Zenaton::Interfaces::Workflow]
86
+ def start_workflow(flow)
87
+ @http.post(
88
+ instance_worker_url,
89
+ ATTR_PROG => PROG,
90
+ ATTR_CANONICAL => canonical_name(flow),
91
+ ATTR_NAME => class_name(flow),
92
+ ATTR_DATA => @serializer.encode(@properties.from(flow)),
93
+ ATTR_ID => parse_custom_id_from(flow)
94
+ )
95
+ end
96
+
97
+ # Stops a workflow
98
+ # @param workflow_name [String] the class name of the workflow
99
+ # @param custom_id [String] the custom ID of the workflow (if any)
100
+ # @return [NilClass]
101
+ def kill_workflow(workflow_name, custom_id)
102
+ update_instance(workflow_name, custom_id, WORKFLOW_KILL)
103
+ end
104
+
105
+ # Pauses a workflow
106
+ # @param workflow_name [String] the class name of the workflow
107
+ # @param custom_id [String] the custom ID of the workflow (if any)
108
+ # @return [NilClass]
109
+ def pause_workflow(workflow_name, custom_id)
110
+ update_instance(workflow_name, custom_id, WORKFLOW_PAUSE)
111
+ end
112
+
113
+ # Resumes a workflow
114
+ # @param workflow_name [String] the class name of the workflow
115
+ # @param custom_id [String] the custom ID of the workflow (if any)
116
+ # @return [NilClass]
117
+ def resume_workflow(workflow_name, custom_id)
118
+ update_instance(workflow_name, custom_id, WORKFLOW_RUN)
119
+ end
120
+
121
+ # Finds a workflow
122
+ # @param workflow_name [String] the class name of the workflow
123
+ # @param custom_id [String] the custom ID of the workflow (if any)
124
+ # @return [Zenaton::Interfaces::Workflow]
125
+ def find_workflow(workflow_name, custom_id)
126
+ # rubocop:disable Metrics/LineLength
127
+ params = "#{ATTR_ID}=#{custom_id}&#{ATTR_NAME}=#{workflow_name}&#{ATTR_PROG}=#{PROG}"
128
+ # rubocop:enable Metrics/LineLength
129
+ data = @http.get(instance_website_url(params))['data']
130
+ data && @properties.object_from(
131
+ data['name'],
132
+ @serializer.decode(data['properties'])
133
+ )
134
+ end
135
+
136
+ # Sends an event to a workflow
137
+ # @param workflow_name [String] the class name of the workflow
138
+ # @param custom_id [String] the custom ID of the workflow (if any)
139
+ # @param event [Zenaton::Interfaces::Event] the event to send
140
+ # @return [NilClass]
141
+ def send_event(workflow_name, custom_id, event)
142
+ body = {
143
+ ATTR_PROG => PROG,
144
+ ATTR_NAME => workflow_name,
145
+ ATTR_ID => custom_id,
146
+ EVENT_NAME => event.class.name,
147
+ EVENT_INPUT => @serializer.encode(@properties.from(event))
148
+ }
149
+ @http.post(send_event_url, body)
150
+ end
151
+
152
+ private
153
+
154
+ def add_app_env(url, params)
155
+ app_env = @app_env ? "#{APP_ENV}=#{@app_env}&" : ''
156
+ app_id = @app_id ? "#{APP_ID}=#{@app_id}&" : ''
157
+
158
+ "#{url}#{app_env}#{app_id}#{params}"
159
+ end
160
+
161
+ def instance_website_url(params)
162
+ website_url('instances', params)
163
+ end
164
+
165
+ def instance_worker_url(params = '')
166
+ worker_url('instances', params)
167
+ end
168
+
169
+ def send_event_url
170
+ worker_url('events')
171
+ end
172
+
173
+ # rubocop:disable Metrics/MethodLength
174
+ def parse_custom_id_from(flow)
175
+ custom_id = flow.id
176
+ if custom_id
177
+ unless custom_id.is_a?(String) || custom_id.is_a?(Integer)
178
+ raise InvalidArgumentError,
179
+ 'Provided ID must be a string or an integer'
180
+ end
181
+ custom_id = custom_id.to_s
182
+ if custom_id.length > MAX_ID_SIZE
183
+ raise InvalidArgumentError,
184
+ "Provided Id must not exceed #{MAX_ID_SIZE} bytes"
185
+ end
186
+ end
187
+ custom_id
188
+ end
189
+ # rubocop:enable Metrics/MethodLength
190
+
191
+ def canonical_name(flow)
192
+ flow.class.name if flow.is_a? Workflows::Version
193
+ end
194
+
195
+ def class_name(flow)
196
+ return flow.class.name unless flow.is_a? Workflows::Version
197
+ flow.current_implementation.class.name
198
+ end
199
+
200
+ def update_instance(workflow_name, custom_id, mode)
201
+ params = "#{ATTR_ID}=#{custom_id}"
202
+ url = instance_worker_url(params)
203
+ options = {
204
+ ATTR_PROG => PROG,
205
+ ATTR_NAME => workflow_name,
206
+ ATTR_MODE => mode
207
+ }
208
+ @http.put(url, options)
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+ require 'zenaton/exceptions'
5
+ require 'zenaton/interfaces/task'
6
+ require 'zenaton/interfaces/workflow'
7
+ require 'zenaton/client'
8
+ require 'zenaton/processor'
9
+
10
+ module Zenaton
11
+ # Zenaton Engine is a singleton class that stores a reference to the current
12
+ # client and processor. It then handles job processing either locally or
13
+ # through the processor with Zenaton workers
14
+ # To access the instance, call `Zenaton::Engine.instance`
15
+ class Engine
16
+ include Singleton
17
+
18
+ # @param value [Zenaton::Processor]
19
+ attr_writer :processor
20
+
21
+ # @private
22
+ def initialize
23
+ @client = Zenaton::Client.instance
24
+ @processor = nil
25
+ end
26
+
27
+ # Executes jobs synchronously
28
+ # @param jobs [Array<Zenaton::Interfaces::Job>]
29
+ # @return [Array<String>, nil] the results if executed locally, or nil
30
+ def execute(jobs)
31
+ jobs.map(&method(:check_argument))
32
+ return jobs.map(&:handle) if process_locally?(jobs)
33
+ @processor.process(jobs, true)
34
+ end
35
+
36
+ # Executes jobs asynchronously
37
+ # @param jobs [Array<Zenaton::Interfaces::Job>]
38
+ # @return nil
39
+ def dispatch(jobs)
40
+ jobs.map(&method(:check_argument))
41
+ jobs.map(&method(:local_dispatch)) if process_locally?(jobs)
42
+ @processor&.process(jobs, false) unless jobs.length.zero?
43
+ nil
44
+ end
45
+
46
+ private
47
+
48
+ def process_locally?(jobs)
49
+ jobs.length.zero? || @processor.nil?
50
+ end
51
+
52
+ def local_dispatch(job)
53
+ if job.is_a? Interfaces::Workflow
54
+ @client.start_workflow(job)
55
+ else
56
+ job.handle
57
+ end
58
+ end
59
+
60
+ def check_argument(job)
61
+ raise InvalidArgumentError, error_message unless valid_job?(job)
62
+ end
63
+
64
+ def error_message
65
+ 'You can only execute or dispatch Zenaton Task or Worflow'
66
+ end
67
+
68
+ def valid_job?(job)
69
+ job.is_a?(Interfaces::Task) || job.is_a?(Interfaces::Workflow)
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zenaton
4
+ # Zenaton base error class
5
+ class Error < StandardError; end
6
+
7
+ # Exception raised when communication with workers failed
8
+ class InternalError < Error; end
9
+
10
+ # Exception raise when clien code is invalid
11
+ class ExternalError < Error; end
12
+
13
+ # Exception raised when wrong argument type is provided
14
+ class InvalidArgumentError < ExternalError; end
15
+
16
+ # :nodoc:
17
+ class UnknownWorkflowError < ExternalError; end
18
+
19
+ # Exception raised when network connectivity is lost
20
+ class ConnectionError < Error; end
21
+
22
+ # Exception raised when interfaces are not fulfilled by client code
23
+ class NotImplemented < Error; end
24
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zenaton
4
+ module Interfaces
5
+ # @abstract Subclass this to define your custom events
6
+ class Event
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zenaton/exceptions'
4
+
5
+ module Zenaton
6
+ # Abstract classes used as interfaces
7
+ module Interfaces
8
+ # @abstract Do not subclass job directly, use either Tasks or Workflows
9
+ class Job
10
+ # Child classes should implement the handle method
11
+ def handle
12
+ raise NotImplemented, "Your job does not implement the `handle' method"
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zenaton/interfaces/job'
4
+
5
+ module Zenaton
6
+ module Interfaces
7
+ # @abstract Subclass and override {#handle} to define your custom tasks
8
+ class Task < Job
9
+ # Child classes should implement the handle method
10
+ def handle
11
+ raise NotImplemented,
12
+ "Your workflow does not implement the `handle' method"
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zenaton/interfaces/job'
4
+
5
+ module Zenaton
6
+ module Interfaces
7
+ # @abstract Subclass and override {#handle} to implement a custom Workflow
8
+ class Workflow < Job
9
+ # Method called to run the workflow
10
+ def handle
11
+ raise NotImplemented,
12
+ "Your workflow does not implement the `handle' method"
13
+ end
14
+
15
+ # (Optional) Implement this method if you want to use custom IDs for your
16
+ # workflows.
17
+ # @return [String, Integer, NilClass] the custom id. Must be <= 256 bytes.
18
+ def id
19
+ nil
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zenaton/engine'
4
+
5
+ module Zenaton
6
+ # Convenience class to execute jobs in parallel
7
+ class Parallel
8
+ # Build a collection of jobs to be executed in parallel
9
+ # @param items [Zenaton::Interfaces::Job]
10
+ def initialize(*items)
11
+ @items = items
12
+ end
13
+
14
+ # Execute synchronous jobs
15
+ def execute
16
+ Engine.instance.execute(@items)
17
+ end
18
+
19
+ # Dispatches asynchronous jobs
20
+ def dispatch
21
+ Engine.instance.dispatch(@items)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zenaton
4
+ # Zenaton Processor
5
+ class Processor
6
+ # processes the jobs
7
+ # @param jobs [Array<Zenaton::Interfaces::Job>]
8
+ # @param bool [Boolean]
9
+ def process(jobs, bool); end
10
+ end
11
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zenaton/exceptions'
4
+ require 'zenaton/client'
5
+ require 'zenaton/interfaces/workflow'
6
+
7
+ module Zenaton
8
+ # Wrapper module for interacting with jobs
9
+ module Query
10
+ # Wrapper class around the client to interact with workflows by id
11
+ class Builder
12
+ def initialize(klass)
13
+ check_klass(klass)
14
+ @klass = klass
15
+ @client = Client.instance
16
+ end
17
+
18
+ # Sets the id of the workflow we want to find
19
+ # @param id [String, NilClass] the id
20
+ # @return [Zenaton::Query::Builder] the current builder
21
+ def where_id(id)
22
+ @id = id
23
+ self
24
+ end
25
+
26
+ # Finds a workflow
27
+ # @return [Zenaton::Interfaces::Workflow]
28
+ def find
29
+ @client.find_workflow(@klass, @id)
30
+ end
31
+
32
+ # Sends an event to a workflow
33
+ # @param event [Zenaton::Interfaces::Event] the event to send
34
+ # @return [Zenaton::Query::Builder] the current builder
35
+ def send_event(event)
36
+ @client.send_event(@klass, @id, event)
37
+ self
38
+ end
39
+
40
+ # Stops a workflow
41
+ # @return [Zenaton::Query::Builder] the current builder
42
+ def kill
43
+ @client.kill_workflow(@klass, @id)
44
+ self
45
+ end
46
+
47
+ # Pauses a workflow
48
+ # @return [Zenaton::Query::Builder] the current builder
49
+ def pause
50
+ @client.pause_workflow(@klass, @id)
51
+ self
52
+ end
53
+
54
+ # Resumes a workflow
55
+ # @return [Zenaton::Query::Builder] the current builder
56
+ def resume
57
+ @client.resume_workflow(@klass, @id)
58
+ self
59
+ end
60
+
61
+ private
62
+
63
+ def check_klass(klass)
64
+ msg = "#{klass} should be a subclass of Zenaton::Interfaces::Workflow"
65
+ raise ExternalError, msg unless klass < Interfaces::Workflow
66
+ end
67
+ end
68
+ end
69
+ end