stenotype 0.1.0 → 0.1.6

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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.rubocop.yml +3 -2
  4. data/CHANGELOG.md +43 -0
  5. data/Gemfile +1 -1
  6. data/Gemfile.lock +111 -60
  7. data/README.md +48 -17
  8. data/Rakefile +2 -2
  9. data/TODO.md +18 -0
  10. data/bin/console +3 -3
  11. data/lib/generators/USAGE +8 -0
  12. data/lib/generators/stenotype/initializer/initializer_generator.rb +23 -0
  13. data/lib/generators/stenotype/initializer/templates/initializer.rb.erb +82 -0
  14. data/lib/stenotype.rb +24 -85
  15. data/lib/stenotype/adapters.rb +3 -3
  16. data/lib/stenotype/adapters/base.rb +25 -2
  17. data/lib/stenotype/adapters/google_cloud.rb +70 -22
  18. data/lib/stenotype/adapters/stdout_adapter.rb +36 -2
  19. data/lib/stenotype/at_exit.rb +8 -0
  20. data/lib/stenotype/configuration.rb +127 -36
  21. data/lib/stenotype/context_handlers.rb +6 -8
  22. data/lib/stenotype/context_handlers/base.rb +14 -3
  23. data/lib/stenotype/context_handlers/collection.rb +78 -34
  24. data/lib/stenotype/context_handlers/rails/active_job.rb +3 -11
  25. data/lib/stenotype/context_handlers/rails/controller.rb +10 -11
  26. data/lib/stenotype/dispatcher.rb +1 -2
  27. data/lib/stenotype/emitter.rb +166 -0
  28. data/lib/stenotype/event.rb +48 -25
  29. data/lib/stenotype/event_serializer.rb +31 -11
  30. data/lib/stenotype/frameworks/rails/action_controller.rb +44 -21
  31. data/lib/stenotype/frameworks/rails/active_job.rb +3 -5
  32. data/lib/stenotype/railtie.rb +37 -0
  33. data/lib/stenotype/version.rb +1 -1
  34. data/stenotype.gemspec +30 -26
  35. metadata +70 -19
  36. data/lib/stenotype/exceptions.rb +0 -31
  37. data/lib/stenotype/frameworks/object_ext.rb +0 -145
data/Rakefile CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'bundler/gem_tasks'
4
- require 'rspec/core/rake_task'
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
5
 
6
6
  RSpec::Core::RakeTask.new(:spec)
7
7
 
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 'bundler/setup'
5
- require 'stenotype'
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 'pry'
14
+ require "pry"
15
15
  Pry.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Generate the Stenotype configuration initializer.
3
+
4
+ Example:
5
+ `rails generate stenotype:initializer`
6
+
7
+ Generates:
8
+ Initializer: config/initializers/stenotype.rb
@@ -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
- # @return {Stenotype::Configuration}
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/exceptions"
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
- config.targets = [
60
- Stenotype::Adapters::StdoutAdapter.new,
61
- Stenotype::Adapters::GoogleCloud.new
62
- ]
63
-
64
- Stenotype::ContextHandlers.module_eval do
65
- register Stenotype::ContextHandlers::Klass
66
- end
67
-
68
- Object.send(:include, Stenotype::Frameworks::ObjectExt)
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
- if defined?(Rails)
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
- ActiveSupport.on_load(:action_controller) do
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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'stenotype/adapters/base'
4
- require 'stenotype/adapters/google_cloud'
5
- require 'stenotype/adapters/stdout_adapter'
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 [NotImplementedError] unless implemented in a subclass
38
+ # @raise {NotImplementedError} unless implemented in a subclass
29
39
  #
30
- def publish(_event_data, **_additional_arguments)
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 'google/cloud/pubsub'
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::Exceptions::GoogleCloudUnsupportedMode} unless the mode
14
- # in configured to be :sync or :async
15
- # @raise {Stenotype::Exceptions::MessageNotPublished} unless message is published
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
- # rubocop:disable Metrics/MethodLength
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, **additional_arguments)
20
- case config.gc_mode
21
- when :async
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
- raise Stenotype::Exceptions::GoogleCloudUnsupportedMode,
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
- # rubocop:enable Metrics/MethodLength
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.gc_topic
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