stenotype 0.1.0 → 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
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