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