stenotype 0.1.0 → 0.1.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/.rubocop.yml +3 -2
- data/CHANGELOG.md +43 -0
- data/Gemfile +1 -1
- data/Gemfile.lock +111 -60
- data/README.md +48 -17
- data/Rakefile +2 -2
- data/TODO.md +18 -0
- data/bin/console +3 -3
- data/lib/generators/USAGE +8 -0
- data/lib/generators/stenotype/initializer/initializer_generator.rb +23 -0
- data/lib/generators/stenotype/initializer/templates/initializer.rb.erb +82 -0
- data/lib/stenotype.rb +24 -85
- data/lib/stenotype/adapters.rb +3 -3
- data/lib/stenotype/adapters/base.rb +25 -2
- data/lib/stenotype/adapters/google_cloud.rb +70 -22
- data/lib/stenotype/adapters/stdout_adapter.rb +36 -2
- data/lib/stenotype/at_exit.rb +8 -0
- data/lib/stenotype/configuration.rb +127 -36
- data/lib/stenotype/context_handlers.rb +6 -8
- data/lib/stenotype/context_handlers/base.rb +14 -3
- data/lib/stenotype/context_handlers/collection.rb +78 -34
- data/lib/stenotype/context_handlers/rails/active_job.rb +3 -11
- data/lib/stenotype/context_handlers/rails/controller.rb +10 -11
- data/lib/stenotype/dispatcher.rb +1 -2
- data/lib/stenotype/emitter.rb +166 -0
- data/lib/stenotype/event.rb +48 -25
- data/lib/stenotype/event_serializer.rb +31 -11
- data/lib/stenotype/frameworks/rails/action_controller.rb +44 -21
- data/lib/stenotype/frameworks/rails/active_job.rb +3 -5
- data/lib/stenotype/railtie.rb +37 -0
- data/lib/stenotype/version.rb +1 -1
- data/stenotype.gemspec +30 -26
- metadata +70 -19
- data/lib/stenotype/exceptions.rb +0 -31
- data/lib/stenotype/frameworks/object_ext.rb +0 -145
data/Rakefile
CHANGED
data/TODO.md
CHANGED
@@ -15,3 +15,21 @@
|
|
15
15
|
- [ ] Figure out the params for plain ruby class context handler.
|
16
16
|
- [ ] Consider `ContextHandlers::ActiveJob` params. How to deal with \_args? It won't necessarily respond to `#as_json`.
|
17
17
|
- [ ] Consider a way to switch from evaluating ruby code to using plain modules extension.
|
18
|
+
|
19
|
+
Feedback TODO:
|
20
|
+
|
21
|
+
- [x] Move all exceptions into root module Stenotype. **Moved exceptions into root module**
|
22
|
+
- [x] Inherit gem specific error from a root error object specific for the gem. **All gem specific error inherit from Stenotype::Errors**
|
23
|
+
- [x] Inherit the root error from standard error. **Stenotype::Errors inherits from StandardError**
|
24
|
+
- [x] Utilize concerns and allow gem users to extend the code they need with a concern rather than aggressively extend Object. **Added an Stenotype::Emitter module instead of aggressively including it into Object**
|
25
|
+
- [x] Use Railtie to introduce rails specific logic instead of checking whether Rails is defined in the root module. **Added a Railtie, active only in Rails world**
|
26
|
+
- [?] Use delegation instead of defining tiny methods. Do not forget about Demeter's. **Rails `delegate` method is used in Rails specific extensions. Otherwise simple methods are used**
|
27
|
+
- [x] Add more examples to yard documentation, consider collecting README.md from the yard doc. **Covered most of the classes/modules with yard examples**
|
28
|
+
- [x] Enable on-off triggers in the configuration to enable/disable framework specific features. Like whether we want to extend ActiveJob or not. **Added two configuration options for currently implemented Rails extensions**
|
29
|
+
- [x] Memoization! **Not actually needed**
|
30
|
+
- [ ] Consider potential double wrapping in the meta-programming stuff. Take a look at around-the-world and consider switching to using it.
|
31
|
+
- [ ] Utilize input objects for attr_readers (what was the name of the tool?)
|
32
|
+
- [ ] Consider configurable for configuration instead of implementing custom configuration object
|
33
|
+
- [ ] Consider using collectible gem to handle collection of context handlers
|
34
|
+
- [ ] Consider naming (e.g. #as_json => #to_h), try to be more specific to not pollute the namespace or introduce any ambiguity.
|
35
|
+
- [ ] Remove freshly mentions from the gem.
|
data/bin/console
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
require
|
5
|
-
require
|
4
|
+
require "bundler/setup"
|
5
|
+
require "stenotype"
|
6
6
|
|
7
7
|
# You can add fixtures and/or initialization code here to make experimenting
|
8
8
|
# with your gem easier. You can also use a different console, if you like.
|
@@ -11,5 +11,5 @@ require 'stenotype'
|
|
11
11
|
# require "pry"
|
12
12
|
# Pry.start
|
13
13
|
|
14
|
-
require
|
14
|
+
require "pry"
|
15
15
|
Pry.start(__FILE__)
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Stenotype
|
4
|
+
#
|
5
|
+
# A module enclosing Rails generators for gem setup
|
6
|
+
#
|
7
|
+
module Generators
|
8
|
+
#
|
9
|
+
# A class for generating a Rails initializer to setup the gem
|
10
|
+
# upon rails application load.
|
11
|
+
#
|
12
|
+
class InitializerGenerator < Rails::Generators::Base
|
13
|
+
source_root File.expand_path("templates", __dir__)
|
14
|
+
|
15
|
+
#
|
16
|
+
# Creates an initializer for rails application
|
17
|
+
#
|
18
|
+
def create_initializer
|
19
|
+
template "initializer.rb.erb", File.join("config/initializers/stenotype.rb")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Rails.application.configure do
|
4
|
+
Stenotype.configure do |config|
|
5
|
+
#
|
6
|
+
# Stenotype allows you to emit events which are then being published to a list of
|
7
|
+
# targets. Currently two targets are provided by default: Google Cloud Pub Sub and
|
8
|
+
# STDOUT for debug purposes.
|
9
|
+
#
|
10
|
+
# There is also a rails integration module which defines a handy DSL
|
11
|
+
# for triggering events in various Rails components. ActionController and
|
12
|
+
# ActiveJob are supported at the time of writing.
|
13
|
+
#
|
14
|
+
# Both extensions for Rails are enabled by default, but there are config options
|
15
|
+
# to control their presence.
|
16
|
+
# config.rails do |rails_modules|
|
17
|
+
# rails_modules.enable_action_controller_ext = true
|
18
|
+
# rails_modules.enable_active_job_ext = true
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# To enable or disable the library use the following config option:
|
22
|
+
#
|
23
|
+
# config.enabled = true # or false
|
24
|
+
#
|
25
|
+
# To make publishing possible one must specify a list of targets. You could use
|
26
|
+
# StdoutAdapter for debug purposes before switching to a production publisher.
|
27
|
+
# By default the list of targets is empty and you'll get an error saying
|
28
|
+
# that no targets are specified.
|
29
|
+
#
|
30
|
+
# A config option is available for setting up the targets:
|
31
|
+
#
|
32
|
+
# config.targets = [Stenotype::Adapters::StdoutAdapter.new]
|
33
|
+
#
|
34
|
+
# Or using Google Cloud:
|
35
|
+
#
|
36
|
+
# config.targets = [Stenotype::Adapters::GoogleCloud.new]
|
37
|
+
#
|
38
|
+
# Or both:
|
39
|
+
#
|
40
|
+
# config.targets = [
|
41
|
+
# Stenotype::Adapters::StdoutAdapter.new,
|
42
|
+
# Stenotype::Adapters::GoogleCloud.new
|
43
|
+
# ]
|
44
|
+
#
|
45
|
+
# To be able to use Google Cloud one has to specify Google Cloud credentials:
|
46
|
+
#
|
47
|
+
# config.google_cloud do |gc_config|
|
48
|
+
# gc_config.credentials = "SPECIFY YOUR CREDENTIALS" # path/to/key.json
|
49
|
+
# gc_config.project_id = "SPECIFY YOUR PROJECT ID"
|
50
|
+
# gc_config.topic = "SPECIFY YOUR TOPIC"
|
51
|
+
# gc_config.async = true
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# Each event is shipped with a UUID generated with SecureRandom by default.
|
55
|
+
# This might be changed by using a corresponding config option. Note that
|
56
|
+
# uuid_generator expects method #uuid to be implemented
|
57
|
+
#
|
58
|
+
# config.uuid_generator = SecureRandom
|
59
|
+
#
|
60
|
+
# In rare cases you might want to get control over how the event is being dispatched.
|
61
|
+
# Dispatcher must implement instance method #publish.
|
62
|
+
# Which dispatcher to use is controlled by the following config option:
|
63
|
+
#
|
64
|
+
# config.dispatcher = Stenotype::Dispatcher
|
65
|
+
#
|
66
|
+
# An option to suppress exception within a gem is available:
|
67
|
+
#
|
68
|
+
# config.graceful_error_handling = true
|
69
|
+
#
|
70
|
+
# To log errors a logger config option is available. Logger.new(STDOUT) is used by default.
|
71
|
+
#
|
72
|
+
# config.logger = Logger.new(STDOUT)
|
73
|
+
#
|
74
|
+
# Add your own context handlers
|
75
|
+
#
|
76
|
+
# Stenotype::ContextHandlers.register Your::Custom::HandlerClass
|
77
|
+
#
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# For more usage instructions please refer to either README.md or yard documentation
|
82
|
+
# in gem repository https://github.com/Freshly/stenotype
|
data/lib/stenotype.rb
CHANGED
@@ -1,98 +1,37 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
# A top level namespace for the freshly-events gem
|
5
|
-
#
|
6
|
-
module Stenotype
|
7
|
-
class << self
|
8
|
-
##
|
9
|
-
# Configures the library.
|
10
|
-
# @yield {Stenotype::Configuration}
|
11
|
-
#
|
12
|
-
# @example
|
13
|
-
#
|
14
|
-
# Stenotype.configure do |config|
|
15
|
-
# config.targets = [
|
16
|
-
# Stenotype::Adapters::StdoutAdapter.new,
|
17
|
-
# Stenotype::Adapters::GoogleCloud.new
|
18
|
-
# ]
|
19
|
-
#
|
20
|
-
# config.dispatcher = Stenotype::Dispatcher.new
|
21
|
-
# config.gc_project_id = ENV['GC_PROJECT_ID']
|
22
|
-
# config.gc_credentials = ENV['GC_CREDENTIALS']
|
23
|
-
# config.gc_topic = ENV['GC_TOPIC']
|
24
|
-
# config.gc_mode = :async
|
25
|
-
# end
|
26
|
-
#
|
27
|
-
def configure(&block)
|
28
|
-
Stenotype::Configuration.configure(&block)
|
29
|
-
end
|
3
|
+
require "securerandom"
|
30
4
|
|
31
|
-
|
32
|
-
|
33
|
-
#
|
34
|
-
def config
|
35
|
-
Stenotype::Configuration
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
5
|
+
require "spicery"
|
6
|
+
require "stenotype/version"
|
39
7
|
|
40
8
|
require "stenotype/adapters"
|
9
|
+
require "stenotype/dispatcher"
|
41
10
|
require "stenotype/configuration"
|
42
11
|
require "stenotype/context_handlers"
|
43
|
-
require "stenotype/dispatcher"
|
44
12
|
require "stenotype/event"
|
45
13
|
require "stenotype/event_serializer"
|
46
|
-
require "stenotype/
|
47
|
-
require "stenotype/version"
|
48
|
-
require "stenotype/frameworks/object_ext"
|
49
|
-
|
50
|
-
Stenotype.configure do |config|
|
51
|
-
config.uuid_generator = SecureRandom
|
52
|
-
config.dispatcher = Stenotype::Dispatcher.new
|
53
|
-
|
54
|
-
config.gc_project_id = ENV['GC_PROJECT_ID']
|
55
|
-
config.gc_credentials = ENV['GC_CREDENTIALS']
|
56
|
-
config.gc_topic = ENV['GC_TOPIC']
|
57
|
-
config.gc_mode = :async
|
14
|
+
require "stenotype/emitter"
|
58
15
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
end
|
67
|
-
|
68
|
-
|
16
|
+
#
|
17
|
+
# A top level namespace for the freshly-events gem
|
18
|
+
#
|
19
|
+
module Stenotype
|
20
|
+
# A wrapper class for Stenotype specific errors
|
21
|
+
class Error < StandardError; end
|
22
|
+
# This exception is being raised upon unsuccessful publishing of an event.
|
23
|
+
class MessageNotPublishedError < Error; end
|
24
|
+
# This exception is being raised in case no targets are
|
25
|
+
# specified {Stenotype::Configuration}.
|
26
|
+
class NoTargetsSpecifiedError < Error; end
|
27
|
+
# This exception is being raised upon using a context handler which
|
28
|
+
# has never been registered in known handlers in {Stenotype::ContextHandlers::Collection}.
|
29
|
+
class UnknownHandlerError < Error; end
|
30
|
+
|
31
|
+
include Spicerack::Configurable::ConfigDelegation
|
32
|
+
delegates_to_configuration
|
69
33
|
end
|
70
34
|
|
71
|
-
|
72
|
-
require "stenotype/frameworks/rails/action_controller"
|
73
|
-
require "stenotype/frameworks/rails/active_job"
|
74
|
-
|
75
|
-
module Stenotype
|
76
|
-
class Railtie < Rails::Railtie # :nodoc:
|
77
|
-
config.stenotype = Stenotype.config
|
78
|
-
|
79
|
-
#
|
80
|
-
# Register Rails handlers
|
81
|
-
#
|
82
|
-
Stenotype::ContextHandlers.module_eval do
|
83
|
-
register Stenotype::ContextHandlers::Rails::Controller
|
84
|
-
register Stenotype::ContextHandlers::Rails::ActiveJob
|
85
|
-
end
|
35
|
+
Stenotype::ContextHandlers.register(Stenotype::ContextHandlers::Klass)
|
86
36
|
|
87
|
-
|
88
|
-
include Stenotype::Frameworks::Rails::ActionControllerExtension
|
89
|
-
end
|
90
|
-
|
91
|
-
ActiveSupport.on_load(:active_job) do
|
92
|
-
# @todo: consider using `::ActiveJob::Base.around_perform`
|
93
|
-
# or `::ActiveJob::Base.around_enqueue`
|
94
|
-
extend Stenotype::Frameworks::Rails::ActiveJobExtension
|
95
|
-
end
|
96
|
-
end
|
97
|
-
end
|
98
|
-
end
|
37
|
+
require "stenotype/railtie" if defined?(Rails)
|
data/lib/stenotype/adapters.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
3
|
+
require "stenotype/adapters/base"
|
4
|
+
require "stenotype/adapters/google_cloud"
|
5
|
+
require "stenotype/adapters/stdout_adapter"
|
@@ -11,6 +11,16 @@ module Stenotype
|
|
11
11
|
# An abstract base class for implementing adapters
|
12
12
|
#
|
13
13
|
# @abstract
|
14
|
+
# @example Defining a custom adapter
|
15
|
+
# MyCustomAdapter < Stenotype::Adapters::Base
|
16
|
+
# def publish(event_data, **additional_arguments)
|
17
|
+
# client.publish(event_data, **additional_arguments)
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# def client
|
21
|
+
# @client ||= SomeCustomClient.new(some_credential)
|
22
|
+
# end
|
23
|
+
# end
|
14
24
|
#
|
15
25
|
class Base
|
16
26
|
attr_reader :client
|
@@ -25,12 +35,25 @@ module Stenotype
|
|
25
35
|
#
|
26
36
|
# This method is expected to be implemented by subclasses
|
27
37
|
# @abstract
|
28
|
-
# @raise
|
38
|
+
# @raise {NotImplementedError} unless implemented in a subclass
|
29
39
|
#
|
30
|
-
def publish(_event_data, **
|
40
|
+
def publish(_event_data, **_additional_attrs)
|
31
41
|
raise NotImplementedError,
|
32
42
|
"#{self.class.name} must implement method #publish"
|
33
43
|
end
|
44
|
+
|
45
|
+
#
|
46
|
+
# This method is expected to be implemented by subclasses. In case async
|
47
|
+
# publisher is used the process might end before the async queue of
|
48
|
+
# messages is processed, so this method is going to be used in a
|
49
|
+
# `at_exit` hook to flush the queue.
|
50
|
+
# @abstract
|
51
|
+
# @raise {NotImplementedError} unless implemented in a subclass
|
52
|
+
#
|
53
|
+
def flush!
|
54
|
+
raise NotImplementedError,
|
55
|
+
"#{self.class.name} must implement method #flush"
|
56
|
+
end
|
34
57
|
end
|
35
58
|
end
|
36
59
|
end
|
@@ -1,55 +1,103 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "google/cloud/pubsub"
|
4
4
|
|
5
5
|
module Stenotype
|
6
6
|
module Adapters
|
7
7
|
#
|
8
8
|
# An adapter implementing method {#publish} to send data to Google Cloud PubSub
|
9
9
|
#
|
10
|
+
# @example A general usage within some method in the class
|
11
|
+
# class EventEmittingClass
|
12
|
+
# def method_emitting_enent
|
13
|
+
# result_of_calculations = collect_some_data
|
14
|
+
# gc_adapter.publish(result_of_calculation, additional: :data, more: :data)
|
15
|
+
# result_of_calculations
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# def gc_adapter
|
19
|
+
# Stenotype::Adapters::GoogleCloud.new
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# @example Overriding a client
|
24
|
+
# class EventEmittingClass
|
25
|
+
# def method_emitting_enent
|
26
|
+
# result_of_calculations = collect_some_data
|
27
|
+
# gc_adapter.publish(result_of_calculation, additional: :data, more: :data)
|
28
|
+
# result_of_calculations
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# def gc_adapter
|
32
|
+
# Stenotype::Adapters::GoogleCloud.new(client: CustomGcClient.new)
|
33
|
+
# end
|
34
|
+
# end
|
35
|
+
#
|
10
36
|
class GoogleCloud < Base
|
37
|
+
attr_reader :topic
|
38
|
+
|
39
|
+
def initialize(client: nil, topic: nil)
|
40
|
+
super(client: client)
|
41
|
+
@topic = topic
|
42
|
+
end
|
11
43
|
#
|
12
44
|
# @param event_data {Hash} The data to be published to Google Cloud
|
13
|
-
# @raise {Stenotype::
|
14
|
-
#
|
15
|
-
# @
|
45
|
+
# @raise {Stenotype::MessageNotPublishedError} unless message is published
|
46
|
+
#
|
47
|
+
# @example With default client
|
48
|
+
# google_cloud_adapter = Stenotype::Adapters::GoogleCloud.new
|
49
|
+
# # publishes to default client
|
50
|
+
# google_cloud_adapter.publish({ event: :data }, { additional: :data })
|
16
51
|
#
|
17
|
-
#
|
52
|
+
# @example With client override
|
53
|
+
# google_cloud_adapter = Stenotype::Adapters::GoogleCloud.new(CustomGCClient.new)
|
54
|
+
# # publishes to default CustomGCClient
|
55
|
+
# google_cloud_adapter.publish({ event: :data }, { additional: :data })
|
18
56
|
#
|
19
|
-
def publish(event_data, **
|
20
|
-
|
21
|
-
|
22
|
-
topic.publish_async(event_data, additional_arguments) do |result|
|
23
|
-
raise Stenotype::Exceptions::MessageNotPublished unless result.succeeded?
|
24
|
-
end
|
25
|
-
when :sync
|
26
|
-
topic.publish(event_data, additional_arguments)
|
57
|
+
def publish(event_data, **additional_attrs)
|
58
|
+
if config.async
|
59
|
+
publish_async(event_data, **additional_attrs)
|
27
60
|
else
|
28
|
-
|
29
|
-
'Please use either :sync or :async modes for publishing the events.'
|
61
|
+
publish_sync(event_data, **additional_attrs)
|
30
62
|
end
|
31
63
|
end
|
32
|
-
|
64
|
+
|
65
|
+
#
|
66
|
+
# Flushes the topic's async queue
|
67
|
+
#
|
68
|
+
def flush!
|
69
|
+
# a publisher might be uninitialized until the first event is published
|
70
|
+
return unless topic.async_publisher
|
71
|
+
|
72
|
+
topic.async_publisher.stop.wait!
|
73
|
+
end
|
33
74
|
|
34
75
|
private
|
35
76
|
|
77
|
+
def publish_sync(event_data, **additional_attrs)
|
78
|
+
topic.publish(event_data, additional_attrs)
|
79
|
+
end
|
80
|
+
|
81
|
+
def publish_async(event_data, **additional_attrs)
|
82
|
+
topic.publish_async(event_data, additional_attrs) do |result|
|
83
|
+
raise Stenotype::MessageNotPublishedError unless result.succeeded?
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
36
87
|
# :nocov:
|
37
88
|
def client
|
38
|
-
@client ||= Google::Cloud::PubSub.new(
|
39
|
-
project_id: config.gc_project_id,
|
40
|
-
credentials: config.gc_credentials
|
41
|
-
)
|
89
|
+
@client ||= Google::Cloud::PubSub.new(project_id: config.project_id, credentials: config.credentials)
|
42
90
|
end
|
43
91
|
|
44
92
|
# Use memoization, otherwise a new topic will be created
|
45
93
|
# every time. And a new async_publisher will be created.
|
46
94
|
# :nocov:
|
47
95
|
def topic
|
48
|
-
@topic ||= client.topic config.
|
96
|
+
@topic ||= client.topic config.topic
|
49
97
|
end
|
50
98
|
|
51
99
|
def config
|
52
|
-
Stenotype.config
|
100
|
+
Stenotype.config.google_cloud
|
53
101
|
end
|
54
102
|
end
|
55
103
|
end
|