stenotype 0.1.0 → 0.1.1

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.
@@ -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,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ Rails.application.configure do
4
+ #
5
+ # Stenotype allows you to emit events which are then being published to a list of
6
+ # targets. Currently two targets are provided by default: Google Cloud Pub Sub and
7
+ # STDOUT for debug purposes.
8
+ #
9
+ # There is also a rails integration module which defines a handy DSL
10
+ # for triggering events in various Rails components. ActionController and
11
+ # ActiveJob are supported at the time of writing.
12
+ #
13
+ # Both extensions for Rails are enabled by default, but there are config options
14
+ # to control their presence.
15
+ # config.stenotype.rails do |rails_modules|
16
+ # rails_modules.enable_action_controller_ext = true
17
+ # rails_modules.enable_active_job_ext = true
18
+ # end
19
+ #
20
+ #
21
+ # To make publishing possible one must specify a list of targets. You could use
22
+ # StdoutAdapter for debug purposes before switching to a production publisher.
23
+ # By default the list of targets is empty and you'll get an error saying
24
+ # that no targets are specified.
25
+ #
26
+ # A config option is available for setting up the targets:
27
+ #
28
+ # config.stenotype.targets = [Stenotype::Adapters::StdoutAdapter.new]
29
+ #
30
+ # Or using Google Cloud:
31
+ #
32
+ # config.stenotype.targets = [Stenotype::Adapters::GoogleCloud.new]
33
+ #
34
+ # Or both:
35
+ #
36
+ # config.stenotype.targets = [
37
+ # Stenotype::Adapters::StdoutAdapter.new,
38
+ # Stenotype::Adapters::GoogleCloud.new
39
+ # ]
40
+ #
41
+ # To be able to use Google Cloud one has to specify Google Cloud credentials:
42
+ #
43
+ # config.stenotype.google_cloud do |gc_config|
44
+ # gc_config.credentials = "SPECIFY YOUR CREDENTIALS" # path/to/key.json
45
+ # gc_config.project_id = "SPECIFY YOUR PROJECT ID"
46
+ # gc_config.topic = "SPECIFY YOUR TOPIC"
47
+ # gc_config.mode = :async
48
+ # end
49
+ #
50
+ # Each event is shipped with a UUID generated with SecureRandom by default.
51
+ # This might be changed by using a corresponding config option. Note that
52
+ # uuid_generator expects method #uuid to be implemented
53
+ #
54
+ # config.stenotype.uuid_generator = SecureRandom
55
+ #
56
+ # In rare cases you might want to get control over how the event is being dispatched.
57
+ # Dispatcher must implement instance method #publish.
58
+ # Which dispatcher to use is controlled by the following config option:
59
+ #
60
+ # config.stenotype.dispatcher = Stenotype::Dispatcher
61
+ #
62
+ end
63
+
64
+ # For more usage instructions please refer to either README.md or yard documentation
65
+ # in gem repository https://github.com/Freshly/stenotype
@@ -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
@@ -1,55 +1,87 @@
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
11
37
  #
12
38
  # @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
39
+ # @raise {Stenotype::MessageNotPublishedError} unless message is published
40
+ #
41
+ # @example With default client
42
+ # google_cloud_adapter = Stenotype::Adapters::GoogleCloud.new
43
+ # # publishes to default client
44
+ # google_cloud_adapter.publish({ event: :data }, { additional: :data })
16
45
  #
17
- # rubocop:disable Metrics/MethodLength
46
+ # @example With client override
47
+ # google_cloud_adapter = Stenotype::Adapters::GoogleCloud.new(CustomGCClient.new)
48
+ # # publishes to default CustomGCClient
49
+ # google_cloud_adapter.publish({ event: :data }, { additional: :data })
18
50
  #
19
51
  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)
52
+ if config.async
53
+ publish_async(event_data, **additional_arguments)
27
54
  else
28
- raise Stenotype::Exceptions::GoogleCloudUnsupportedMode,
29
- 'Please use either :sync or :async modes for publishing the events.'
55
+ publish_sync(event_data, **additional_arguments)
30
56
  end
31
57
  end
32
- # rubocop:enable Metrics/MethodLength
33
58
 
34
59
  private
35
60
 
61
+ def publish_sync(event_data, **additional_arguments)
62
+ topic.publish(event_data, additional_arguments)
63
+ end
64
+
65
+ def publish_async(event_data, **additional_arguments)
66
+ topic.publish_async(event_data, additional_arguments) do |result|
67
+ raise Stenotype::MessageNotPublishedError unless result.succeeded?
68
+ end
69
+ end
70
+
36
71
  # :nocov:
37
72
  def client
38
- @client ||= Google::Cloud::PubSub.new(
39
- project_id: config.gc_project_id,
40
- credentials: config.gc_credentials
41
- )
73
+ @client ||= Google::Cloud::PubSub.new(project_id: config.project_id, credentials: config.credentials)
42
74
  end
43
75
 
44
76
  # Use memoization, otherwise a new topic will be created
45
77
  # every time. And a new async_publisher will be created.
46
78
  # :nocov:
47
79
  def topic
48
- @topic ||= client.topic config.gc_topic
80
+ @topic ||= client.topic config.topic
49
81
  end
50
82
 
51
83
  def config
52
- Stenotype.config
84
+ Stenotype.config.google_cloud
53
85
  end
54
86
  end
55
87
  end
@@ -5,12 +5,34 @@ module Stenotype
5
5
  #
6
6
  # An adapter implementing method {#publish} to send data to STDOUT
7
7
  #
8
+ # @example
9
+ # class SomeClassWithEvents
10
+ # def method_emitting_enent
11
+ # result_of_calculations = collect_some_data
12
+ # # This will print the data to STDOUT by default
13
+ # stdout_adapter.publish(result_of_calculation, additional: :data, more: :data)
14
+ # result_of_calculations
15
+ # end
16
+ #
17
+ # def stdout_adapter
18
+ # Stenotype::Adapters::StdoutAdapter.new
19
+ # end
20
+ # end
21
+ #
8
22
  class StdoutAdapter < Base
9
23
  #
10
24
  # @param event_data {Hash} The data to be published to STDOUT
11
25
  #
26
+ # @example Publishing to default client (STDOUT)
27
+ # adapter = Stenotype::Adapters::StdoutAdapter.new
28
+ # adapter.publish({ event: :data }, { additional: :data })
29
+ #
30
+ # @example Publishing to custom client (STDERR)
31
+ # adapter = Stenotype::Adapters::StdoutAdapter.new(client: STDERR)
32
+ # adapter.publish({ event: :data }, { additional: :data })
33
+ #
12
34
  def publish(event_data, **additional_arguments)
13
- client.info(event_data, **additional_arguments)
35
+ client.info(**event_data, **additional_arguments)
14
36
  end
15
37
 
16
38
  private
@@ -4,46 +4,101 @@ module Stenotype
4
4
  #
5
5
  # A module containing freshly-event gem configuration
6
6
  #
7
+ # @example Configuring the library
8
+ # Stenotype.configure do |config|
9
+ # config.targets = [Target1.new, Target2.new]
10
+ # config.uuid_generator = SecureRandom
11
+ #
12
+ # config.google_cloud do |gc|
13
+ # gc.credentials = "abc"
14
+ # gc.project_id = "project"
15
+ # gc.topic = "42"
16
+ # gc.async = true
17
+ # end
18
+ #
19
+ # config.rails do |rc|
20
+ # rc.enable_action_controller_ext = true
21
+ # rc.enable_active_job_ext = false
22
+ # end
23
+ # end
24
+ #
7
25
  module Configuration
8
- class << self
9
- # @return {Array<#publish>} a list of targets responding to method [#publish]
10
- attr_writer :targets
11
- # @return {Sting} a string with GC API credential. Refer to GC PubSub documentation
12
- attr_accessor :gc_credentials
13
- # @return {String} a name of the project in GC PubSub
14
- attr_accessor :gc_project_id
15
- # @return {String} a name of the topic in GC PubSub
16
- attr_accessor :gc_topic
17
- # @return [:sync, :async] GC publish mode
18
- attr_accessor :gc_mode
19
- # @return {#publish} as object responding to method [#publish]
20
- attr_accessor :dispatcher
21
- # @return {#uuid} an object responding to method [#uuid]
22
- attr_accessor :uuid_generator
23
-
24
- #
25
- # Yields control to the caller
26
- # @yield {Stenotype::Configuration}
27
- # @return {Stenotype::Configuration}
28
- #
29
- def configure
30
- yield self
31
- self
26
+ extend Spicerack::Configurable
27
+
28
+ # @!attribute targets
29
+ # @return {Array<#publish>} a list of targets responding to method [#publish]
30
+
31
+ # @!attribute [rw] dispatcher
32
+ # @return {#publish} as object responding to method [#publish]
33
+
34
+ # @!attribute [rw] uuid_generator
35
+ # @return {#uuid} an object responding to method [#uuid]
36
+
37
+ # @!attribute [rw] google_cloud
38
+ # @return [NestedConfiguration] google cloud configuration.
39
+
40
+ # @!attribute [rw] google_cloud.credentials
41
+ # @return {String} a string with GC API credential. Refer to GC PubSub documentation
42
+
43
+ # @!attribute [rw] google_cloud.project_id
44
+ # @return {String} a name of the project in GC PubSub
45
+
46
+ # @!attribute [rw] google_cloud.topic
47
+ # @return {String} a name of the topic in GC PubSub
48
+
49
+ # @!attribute [rw] google_cloud.async
50
+ # @return [true, false] GC publish mode, either async if true, sync if false
51
+
52
+
53
+ # @!attribute [rw] rails
54
+ # @return [NestedConfiguration] Rails configuration.
55
+
56
+ # @!attribute [rw] rails.enable_action_controller_ext
57
+ # @return [true, false] A flag of whether ActionController ext is enabled
58
+
59
+ # @!attribute [rw] rails.enable_active_job_ext
60
+ # @return [true, false] A flag of whether ActiveJob ext is enabled
61
+
62
+ configuration_options do
63
+ option :targets, default: []
64
+ option :dispatcher, default: Stenotype::Dispatcher
65
+ option :uuid_generator, default: SecureRandom
66
+
67
+ nested :google_cloud do
68
+ option :credentials, default: nil
69
+ option :project_id, default: nil
70
+ option :topic, default: nil
71
+ option :async, default: true
32
72
  end
33
73
 
34
- #
35
- # @raise {Stenotype::Exceptions::NoTargetsSpecified} in case no targets are configured
36
- # @return {Array<#publish>} An array of targets implementing method [#publish]
37
- #
38
- def targets
39
- if @targets.nil? || @targets.empty?
40
- raise Stenotype::Exceptions::NoTargetsSpecified,
41
- 'Please configure a target(s) for events to be sent to. ' \
42
- 'See Stenotype::Configuration for reference.'
43
- else
44
- @targets
45
- end
74
+ nested :rails do
75
+ option :enable_action_controller_ext, default: true
76
+ option :enable_active_job_ext, default: true
46
77
  end
47
78
  end
79
+
80
+ module_function
81
+
82
+ #
83
+ # @example When at least one target is present
84
+ # Stenotype.configure do |config|
85
+ # config.targets = [Target1.new, Target2.new]
86
+ # end
87
+ # Stenotype.config.targets #=> [target_obj1, target_obj2]
88
+ #
89
+ # @example When no targets have been configured
90
+ # Stenotype.configure { |config| config.targets = [] }
91
+ # Stenotype.config.targets #=> Stenotype::NoTargetsSpecifiedError
92
+ #
93
+ # @raise {Stenotype::NoTargetsSpecifiedError} in case no targets are configured
94
+ # @return {Array<#publish>} An array of targets implementing method [#publish]
95
+ #
96
+ def targets
97
+ return config.targets unless config.targets.empty?
98
+
99
+ raise Stenotype::NoTargetsSpecifiedError,
100
+ "Please configure a target(s) for events to be sent to. "\
101
+ "See #{Stenotype::Configuration} for reference."
102
+ end
48
103
  end
49
104
  end