logux-rack 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop_todo.yml +17 -0
  3. data/CHANGELOG.md +9 -0
  4. data/Gemfile +8 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +130 -0
  7. data/Rakefile +12 -0
  8. data/bin/console +15 -0
  9. data/bin/setup +8 -0
  10. data/config.ru +6 -0
  11. data/docker-compose.yml +29 -0
  12. data/lib/logux.rb +3 -0
  13. data/lib/logux/action.rb +68 -0
  14. data/lib/logux/action_caller.rb +46 -0
  15. data/lib/logux/action_controller.rb +6 -0
  16. data/lib/logux/action_watcher.rb +21 -0
  17. data/lib/logux/actions.rb +17 -0
  18. data/lib/logux/add.rb +37 -0
  19. data/lib/logux/auth.rb +13 -0
  20. data/lib/logux/base_controller.rb +37 -0
  21. data/lib/logux/channel_controller.rb +24 -0
  22. data/lib/logux/class_finder.rb +63 -0
  23. data/lib/logux/client.rb +21 -0
  24. data/lib/logux/error_renderer.rb +40 -0
  25. data/lib/logux/meta.rb +36 -0
  26. data/lib/logux/node.rb +37 -0
  27. data/lib/logux/policy.rb +14 -0
  28. data/lib/logux/policy_caller.rb +36 -0
  29. data/lib/logux/process.rb +9 -0
  30. data/lib/logux/process/action.rb +60 -0
  31. data/lib/logux/process/auth.rb +31 -0
  32. data/lib/logux/process/batch.rb +59 -0
  33. data/lib/logux/rack.rb +133 -0
  34. data/lib/logux/rack/app.rb +55 -0
  35. data/lib/logux/rack/version.rb +7 -0
  36. data/lib/logux/rake_tasks.rb +118 -0
  37. data/lib/logux/response.rb +18 -0
  38. data/lib/logux/stream.rb +27 -0
  39. data/lib/logux/test.rb +35 -0
  40. data/lib/logux/test/helpers.rb +73 -0
  41. data/lib/logux/test/matchers.rb +10 -0
  42. data/lib/logux/test/matchers/base.rb +25 -0
  43. data/lib/logux/test/matchers/response_chunks.rb +48 -0
  44. data/lib/logux/test/matchers/send_to_logux.rb +51 -0
  45. data/lib/logux/test/store.rb +21 -0
  46. data/lib/logux/utils.rb +19 -0
  47. data/lib/tasks/logux_tasks.rake +4 -0
  48. data/logux-rack.gemspec +68 -0
  49. metadata +386 -0
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Logux
4
+ module Process
5
+ class Batch
6
+ attr_reader :stream, :batch
7
+
8
+ def initialize(stream:, batch:)
9
+ @stream = stream
10
+ @batch = batch
11
+ end
12
+
13
+ def call
14
+ last_chunk = batch.size - 1
15
+ preprocessed_batch.map.with_index do |chunk, index|
16
+ case chunk[:type]
17
+ when :action
18
+ process_action(chunk: chunk.slice(:action, :meta))
19
+ when :auth
20
+ process_auth(chunk: chunk[:auth])
21
+ end
22
+ stream.write(',') if index != last_chunk
23
+ end
24
+ end
25
+
26
+ def process_action(chunk:)
27
+ Logux::Process::Action.new(stream: stream, chunk: chunk).call
28
+ end
29
+
30
+ def process_auth(chunk:)
31
+ Logux::Process::Auth.new(stream: stream, chunk: chunk).call
32
+ end
33
+
34
+ def preprocessed_batch
35
+ @preprocessed_batch ||= batch.map do |chunk|
36
+ case chunk[0]
37
+ when 'action'
38
+ preprocess_action(chunk)
39
+ when 'auth'
40
+ preprocess_auth(chunk)
41
+ end
42
+ end
43
+ end
44
+
45
+ def preprocess_action(chunk)
46
+ { type: :action,
47
+ action: Logux::Action.new(chunk[1]),
48
+ meta: Logux::Meta.new(chunk[2]) }
49
+ end
50
+
51
+ def preprocess_auth(chunk)
52
+ { type: :auth,
53
+ auth: Logux::Auth.new(user_id: chunk[1],
54
+ credentials: chunk[2],
55
+ auth_id: chunk[3]) }
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'colorize'
4
+ require 'configurations'
5
+ require 'forwardable'
6
+ require 'json'
7
+ require 'logger'
8
+ require 'nanoid'
9
+ require 'rest-client'
10
+ require 'sinatra/base'
11
+ require 'singleton'
12
+
13
+ module Logux
14
+ include Configurations
15
+
16
+ PROTOCOL_VERSION = 1
17
+
18
+ class WithMetaError < StandardError
19
+ attr_reader :meta
20
+
21
+ def initialize(msg, meta: nil)
22
+ @meta = meta
23
+ super(msg)
24
+ end
25
+ end
26
+
27
+ UnknownActionError = Class.new(WithMetaError)
28
+ UnknownChannelError = Class.new(WithMetaError)
29
+ UnauthorizedError = Class.new(StandardError)
30
+ ParameterMissingError = Class.new(StandardError)
31
+
32
+ autoload :Client, 'logux/client'
33
+ autoload :Meta, 'logux/meta'
34
+ autoload :Action, 'logux/action'
35
+ autoload :Actions, 'logux/actions'
36
+ autoload :Auth, 'logux/auth'
37
+ autoload :BaseController, 'logux/base_controller'
38
+ autoload :ActionController, 'logux/action_controller'
39
+ autoload :ChannelController, 'logux/channel_controller'
40
+ autoload :ClassFinder, 'logux/class_finder'
41
+ autoload :ActionCaller, 'logux/action_caller'
42
+ autoload :PolicyCaller, 'logux/policy_caller'
43
+ autoload :Policy, 'logux/policy'
44
+ autoload :Add, 'logux/add'
45
+ autoload :Node, 'logux/node'
46
+ autoload :Response, 'logux/response'
47
+ autoload :Stream, 'logux/stream'
48
+ autoload :Process, 'logux/process'
49
+ autoload :Version, 'logux/version'
50
+ autoload :Test, 'logux/test'
51
+ autoload :ErrorRenderer, 'logux/error_renderer'
52
+ autoload :Utils, 'logux/utils'
53
+ autoload :ActionWatcher, 'logux/action_watcher'
54
+
55
+ configurable %i[
56
+ action_watcher
57
+ action_watcher_options
58
+ auth_rule
59
+ logger
60
+ logux_host
61
+ on_error
62
+ password
63
+ render_backtrace_on_error
64
+ verify_authorized
65
+ ]
66
+
67
+ configuration_defaults do |config|
68
+ config.logux_host = 'localhost:1338'
69
+ config.verify_authorized = true
70
+ config.logger = ::Logger.new(STDOUT)
71
+ config.on_error = proc {}
72
+ config.auth_rule = proc { false }
73
+ config.render_backtrace_on_error = true
74
+ config.action_watcher = Logux::ActionWatcher
75
+ config.action_watcher_options = {}
76
+ end
77
+
78
+ module Rack
79
+ autoload :App, 'logux/rack/app'
80
+ end
81
+
82
+ class << self
83
+ def add(action, meta = Meta.new)
84
+ Logux::Add.new.call([[action, meta]])
85
+ end
86
+
87
+ def add_batch(commands)
88
+ Logux::Add.new.call(commands)
89
+ end
90
+
91
+ def undo(meta, reason: nil, data: {})
92
+ add(
93
+ data.merge(type: 'logux/undo', id: meta.id, reason: reason),
94
+ Logux::Meta.new(clients: [meta.client_id])
95
+ )
96
+ end
97
+
98
+ def verify_request_meta_data(meta_params)
99
+ if configuration.password.nil?
100
+ logger.warn(%(Please, add password for logux server:
101
+ Logux.configure do |c|
102
+ c.password = 'your-password'
103
+ end))
104
+ end
105
+ auth = configuration.password == meta_params&.dig('password')
106
+ raise UnauthorizedError, 'Incorrect password' unless auth
107
+ end
108
+
109
+ def process_batch(stream:, batch:)
110
+ Logux::Process::Batch.new(stream: stream, batch: batch).call
111
+ end
112
+
113
+ def generate_action_id
114
+ Logux::Node.instance.generate_action_id
115
+ end
116
+
117
+ def logger
118
+ configuration.logger
119
+ end
120
+
121
+ def action_watcher
122
+ configuration.action_watcher.new(configuration.action_watcher_options)
123
+ end
124
+
125
+ def watch_action
126
+ action_watcher.call { yield }
127
+ end
128
+
129
+ def application
130
+ Logux::Rack::App
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Logux
4
+ module Rack
5
+ LOGUX_ROOT_PATH = '/logux'
6
+
7
+ class App < Sinatra::Base
8
+ before do
9
+ request.body.rewind
10
+ content_type 'application/json'
11
+ end
12
+
13
+ post LOGUX_ROOT_PATH do
14
+ stream do |out|
15
+ begin
16
+ logux_stream = Logux::Stream.new(out)
17
+ logux_stream.write('[')
18
+ Logux.verify_request_meta_data(meta_params)
19
+ Logux.process_batch(stream: logux_stream, batch: command_params)
20
+ rescue => e
21
+ handle_processing_errors(logux_stream, e)
22
+ ensure
23
+ logux_stream.write(']')
24
+ logux_stream.close
25
+ end
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def logux_params
32
+ @logux_params ||= JSON.parse(request.body.read)
33
+ end
34
+
35
+ def command_params
36
+ logux_params.dig('commands') || []
37
+ end
38
+
39
+ def meta_params
40
+ logux_params&.slice('version', 'password')
41
+ end
42
+
43
+ def handle_processing_errors(logux_stream, exception)
44
+ Logux.configuration.on_error&.call(exception)
45
+ Logux.logger.error("#{exception}\n#{exception.backtrace.join("\n")}")
46
+ ensure
47
+ logux_stream.write(Logux::ErrorRenderer.new(exception).message)
48
+ end
49
+ end
50
+ end
51
+
52
+ def self.application
53
+ Logux::Rack::App
54
+ end
55
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Logux
4
+ module Rack
5
+ VERSION = '0.1.0'
6
+ end
7
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rake'
4
+ require 'rake/tasklib'
5
+ require_relative 'rack'
6
+
7
+ module Logux
8
+ class RakeTasks < ::Rake::TaskLib
9
+ attr_accessor :name
10
+ attr_accessor :verbose
11
+ attr_accessor :fail_on_error
12
+ attr_accessor :patterns
13
+ attr_accessor :formatters
14
+ attr_accessor :requires
15
+ attr_accessor :options
16
+
17
+ using Logux::Utils
18
+
19
+ ACTIONS_NAMESPACE = 'Actions'
20
+ CHANNELS_NAMESPACE = 'Channels'
21
+
22
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
23
+ def initialize(name = :logux)
24
+ setup(name)
25
+ namespace(name) do
26
+ desc 'Lists all Logux action types'
27
+ task(:actions, [:actions_path]) do |_, task_args|
28
+ task_args.with_defaults(actions_path: default_actions_path)
29
+ require_all(task_args.actions_path)
30
+ report(ACTIONS_HEAD, action_types)
31
+ end
32
+
33
+ desc 'Lists all Logux channel'
34
+ task(:channels, [:channels_path]) do |_, task_args|
35
+ task_args.with_defaults(channels_path: default_channels_path)
36
+ require_all(task_args.channels_path)
37
+ report(CHANNELS_HEAD, channels)
38
+ end
39
+ end
40
+ end
41
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
42
+
43
+ protected
44
+
45
+ def default_actions_path
46
+ File.join(Dir.pwd, '**', 'actions')
47
+ end
48
+
49
+ def default_channels_path
50
+ File.join(Dir.pwd, '**', 'channels')
51
+ end
52
+
53
+ private
54
+
55
+ def setup(name)
56
+ @name = name
57
+ @verbose = true
58
+ @fail_on_error = true
59
+ @patterns = []
60
+ @requires = []
61
+ @options = []
62
+ @formatters = []
63
+ end
64
+
65
+ ACTIONS_HEAD = [%w[action.type Class#method]].freeze
66
+ CHANNELS_HEAD = [%w[channel Class]].freeze
67
+ private_constant :ACTIONS_HEAD, :CHANNELS_HEAD
68
+
69
+ def require_all(path)
70
+ Dir[File.join(path, '**', '*.rb')].each do |file|
71
+ require file
72
+ end
73
+ end
74
+
75
+ def action_types
76
+ classes = descendants_of(::Logux::ActionController).sort_by(&:name)
77
+ classes.flat_map { |klass| action_methods(klass) }
78
+ end
79
+
80
+ def action_methods(klass)
81
+ prefix = action_type(klass)
82
+ klass.instance_methods(false).sort.map do |action|
83
+ [[prefix, action].join('/'), [klass.name, action].join('#')]
84
+ end
85
+ end
86
+
87
+ def action_type(klass)
88
+ strip_namespace(klass, ACTIONS_NAMESPACE)
89
+ end
90
+
91
+ def channels
92
+ classes = descendants_of(::Logux::ChannelController)
93
+ classes.select(&:name).sort_by(&:name).map do |klass|
94
+ [channel(klass), klass]
95
+ end
96
+ end
97
+
98
+ def channel(klass)
99
+ strip_namespace(klass, CHANNELS_NAMESPACE)
100
+ end
101
+
102
+ def strip_namespace(klass, namespace)
103
+ klass.name.gsub(/^#{namespace}::/, '').underscore
104
+ end
105
+
106
+ def descendants_of(parent)
107
+ ObjectSpace.each_object(Class).select { |klass| klass < parent }
108
+ end
109
+
110
+ def report(header, items)
111
+ output = header + items
112
+ first_column_length = output.map(&:first).max_by(&:length).length
113
+ output.each do |entity, klass_name|
114
+ puts "#{entity.rjust(first_column_length, ' ')} #{klass_name}"
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Logux
4
+ class Response
5
+ attr_reader :status, :action, :meta, :custom_data
6
+
7
+ def initialize(status, action:, meta:, custom_data: nil)
8
+ @status = status
9
+ @action = action
10
+ @meta = meta
11
+ @custom_data = custom_data
12
+ end
13
+
14
+ def format
15
+ [status, custom_data || meta.id]
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Logux
4
+ class Stream
5
+ extend Forwardable
6
+
7
+ attr_reader :stream
8
+
9
+ def_delegators :stream, :close
10
+
11
+ def initialize(stream)
12
+ @stream = stream
13
+ end
14
+
15
+ def write(payload)
16
+ processed_payload = process(payload)
17
+ Logux.logger.debug("Write to Logux response: #{processed_payload}")
18
+ stream << processed_payload
19
+ end
20
+
21
+ private
22
+
23
+ def process(payload)
24
+ payload.is_a?(::String) ? payload : payload.to_json
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Logux
4
+ module Test
5
+ class << self
6
+ attr_accessor :http_requests_enabled
7
+
8
+ def enable_http_requests!
9
+ raise ArgumentError unless block_given?
10
+
11
+ begin
12
+ self.http_requests_enabled = true
13
+ yield
14
+ ensure
15
+ self.http_requests_enabled = false
16
+ end
17
+ end
18
+ end
19
+
20
+ module Client
21
+ def post(params)
22
+ if Logux::Test.http_requests_enabled
23
+ super(params)
24
+ else
25
+ Logux::Test::Store.instance.add(params.to_json)
26
+ end
27
+ end
28
+ end
29
+
30
+ autoload :Helpers, 'logux/test/helpers'
31
+ autoload :Store, 'logux/test/store'
32
+ autoload :Matchers, 'logux/test/matchers'
33
+ end
34
+ end
35
+ Logux::Client.prepend Logux::Test::Client