logux-rack 0.1.0

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 (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,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Logux
4
+ class Add
5
+ attr_reader :client, :version, :password
6
+
7
+ def initialize(client: Logux::Client.new,
8
+ version: Logux::PROTOCOL_VERSION,
9
+ password: Logux.configuration.password)
10
+ @client = client
11
+ @version = version
12
+ @password = password
13
+ end
14
+
15
+ def call(commands)
16
+ return if commands.empty?
17
+
18
+ prepared_data = prepare_data(commands)
19
+ Logux.logger.debug("Logux add: #{prepared_data}")
20
+ client.post(prepared_data)
21
+ end
22
+
23
+ private
24
+
25
+ def prepare_data(commands)
26
+ {
27
+ version: PROTOCOL_VERSION,
28
+ password: password,
29
+ commands: commands.map do |command|
30
+ action = command.first
31
+ meta = command[1]
32
+ ['action', action, meta || Meta.new]
33
+ end
34
+ }
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Logux
4
+ class Auth
5
+ attr_reader :user_id, :credentials, :auth_id
6
+
7
+ def initialize(user_id:, credentials:, auth_id:)
8
+ @user_id = user_id
9
+ @credentials = credentials
10
+ @auth_id = auth_id
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Logux
4
+ class BaseController
5
+ class << self
6
+ def verify_authorized!
7
+ Logux.configuration.verify_authorized = true
8
+ end
9
+
10
+ def unverify_authorized!
11
+ Logux.configuration.verify_authorized = false
12
+ end
13
+ end
14
+
15
+ attr_reader :action, :meta
16
+
17
+ def initialize(action:, meta: {})
18
+ @action = action
19
+ @meta = meta
20
+ end
21
+
22
+ def respond(status, action: @action, meta: @meta, custom_data: nil)
23
+ Logux::Response.new(status,
24
+ action: action,
25
+ meta: meta,
26
+ custom_data: custom_data)
27
+ end
28
+
29
+ def user_id
30
+ @user_id ||= meta.user_id
31
+ end
32
+
33
+ def node_id
34
+ @node_id ||= meta.node_id
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Logux
4
+ class ChannelController < BaseController
5
+ def subscribe
6
+ Logux.add_batch(initial_data.map { |d| [d, initial_meta] })
7
+ end
8
+
9
+ def initial_data
10
+ []
11
+ end
12
+
13
+ def initial_meta
14
+ { clients: [meta.client_id] }
15
+ end
16
+
17
+ def since_time
18
+ @since_time ||= begin
19
+ since = action[:since]&.send(:[], :time)
20
+ Time.at(since).to_datetime if since
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Logux
4
+ class ClassFinder
5
+ attr_reader :action, :meta
6
+
7
+ using Logux::Utils
8
+
9
+ def initialize(action:, meta:)
10
+ @action = action
11
+ @meta = meta
12
+ end
13
+
14
+ def find_action_class
15
+ "#{class_namespace}::#{class_name}".constantize
16
+ rescue NameError
17
+ message =
18
+ "Unable to find action #{class_name.camelize}.\n" \
19
+ "Should be in app/logux/#{class_namespace.downcase}/#{class_path}.rb"
20
+ raise_error_for_failed_find(message)
21
+ end
22
+
23
+ def find_policy_class
24
+ "Policies::#{class_namespace}::#{class_name}".constantize
25
+ rescue NameError
26
+ message =
27
+ "Unable to find action policy #{class_name.camelize}.\n" \
28
+ "Should be in app/logux/#{class_namespace.downcase}/#{class_path}.rb"
29
+ raise_error_for_failed_find(message)
30
+ end
31
+
32
+ def class_name
33
+ if subscribe?
34
+ action.channel_name.camelize
35
+ else
36
+ action.type.split('/')[0..-2].map(&:camelize).join('::')
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def class_namespace
43
+ subscribe? ? 'Channels' : 'Actions'
44
+ end
45
+
46
+ def subscribe?
47
+ action.type == 'logux/subscribe'
48
+ end
49
+
50
+ def action?
51
+ !subscribe?
52
+ end
53
+
54
+ def class_path
55
+ "#{class_namespace}::#{class_name}".underscore
56
+ end
57
+
58
+ def raise_error_for_failed_find(message)
59
+ exception_class = action? ? UnknownActionError : UnknownChannelError
60
+ raise exception_class.new(message, meta: meta)
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Logux
4
+ class Client
5
+ attr_reader :logux_host
6
+
7
+ def initialize(logux_host: Logux.configuration.logux_host)
8
+ @logux_host = logux_host
9
+ end
10
+
11
+ def post(params)
12
+ client.post(params.to_json,
13
+ content_type: :json,
14
+ accept: :json)
15
+ end
16
+
17
+ def client
18
+ @client ||= RestClient::Resource.new(logux_host, verify_ssl: false)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Logux
4
+ class ErrorRenderer
5
+ attr_reader :exception
6
+
7
+ def initialize(exception)
8
+ @exception = exception
9
+ end
10
+
11
+ def message
12
+ case exception
13
+ when Logux::WithMetaError
14
+ build_message(exception, exception.meta.id)
15
+ when Logux::UnauthorizedError
16
+ build_message(exception, exception.message)
17
+ when StandardError
18
+ # some runtime error that should be fixed
19
+ render_stardard_error(exception)
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def render_stardard_error(exception)
26
+ if Logux.configuration.render_backtrace_on_error
27
+ ['error', exception.message + "\n" + exception.backtrace.join("\n")]
28
+ else
29
+ ['error', 'Please check server logs for more information']
30
+ end
31
+ end
32
+
33
+ def build_message(exception, additional_info)
34
+ [
35
+ exception.class.name.demodulize.camelize(:lower).gsub(/Error/, ''),
36
+ additional_info
37
+ ]
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Logux
4
+ class Meta < Hash
5
+ def initialize(source_hash = {})
6
+ merge!(source_hash.stringify_keys)
7
+
8
+ self['id'] ||= Logux.generate_action_id
9
+ self['time'] ||= self['id'].split(' ')[0]
10
+ end
11
+
12
+ def node_id
13
+ id.split(' ')[1]
14
+ end
15
+
16
+ def user_id
17
+ node_id.split(':')[0]
18
+ end
19
+
20
+ def client_id
21
+ node_id.split(':')[0..1].join(':')
22
+ end
23
+
24
+ def logux_order
25
+ time + ' ' + id.split(' ')[1..-1].join(' ')
26
+ end
27
+
28
+ def time
29
+ fetch('time')
30
+ end
31
+
32
+ def id
33
+ fetch('id')
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Logux
4
+ class Node
5
+ include ::Singleton
6
+
7
+ attr_accessor :last_time, :sequence
8
+ attr_writer :node_id
9
+
10
+ def generate_action_id
11
+ mutex.synchronize do
12
+ if last_time && now_time <= last_time
13
+ @sequence += 1
14
+ else
15
+ @sequence = 0
16
+ @last_time = now_time
17
+ end
18
+
19
+ "#{last_time} #{node_id} #{sequence}"
20
+ end
21
+ end
22
+
23
+ def node_id
24
+ @node_id ||= "server:#{Nanoid.generate(size: 8)}"
25
+ end
26
+
27
+ private
28
+
29
+ def now_time
30
+ Time.now.to_datetime.strftime('%Q')
31
+ end
32
+
33
+ def mutex
34
+ @mutex ||= Mutex.new
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Logux
4
+ class Policy
5
+ class UnauthorizedError < StandardError; end
6
+
7
+ attr_reader :action, :meta
8
+
9
+ def initialize(action:, meta:)
10
+ @action = action
11
+ @meta = meta
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Logux
4
+ class PolicyCaller
5
+ extend Forwardable
6
+
7
+ attr_reader :action, :meta
8
+
9
+ def_delegators :Logux, :logger, :configuration
10
+
11
+ def initialize(action:, meta:)
12
+ @action = action
13
+ @meta = meta
14
+ end
15
+
16
+ def call!
17
+ logger.debug('Searching policy for Logux action:' \
18
+ " #{action}, meta: #{meta}")
19
+ policy.public_send("#{action.action_type}?")
20
+ rescue Logux::UnknownActionError, Logux::UnknownChannelError => e
21
+ raise e if configuration.verify_authorized
22
+
23
+ logger.warn(e)
24
+ end
25
+
26
+ private
27
+
28
+ def class_finder
29
+ @class_finder ||= Logux::ClassFinder.new(action: action, meta: meta)
30
+ end
31
+
32
+ def policy
33
+ class_finder.find_policy_class.new(action: action, meta: meta)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Logux
4
+ module Process
5
+ autoload :Batch, 'logux/process/batch'
6
+ autoload :Auth, 'logux/process/auth'
7
+ autoload :Action, 'logux/process/action'
8
+ end
9
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Logux
4
+ module Process
5
+ class Action
6
+ attr_reader :stream, :chunk
7
+ attr_accessor :stop_process
8
+
9
+ def initialize(stream:, chunk:)
10
+ @stream = stream
11
+ @chunk = chunk
12
+ end
13
+
14
+ def call
15
+ process_authorization!
16
+ process_action!
17
+ end
18
+
19
+ def action_from_chunk
20
+ @action_from_chunk ||= chunk[:action]
21
+ end
22
+
23
+ def meta_from_chunk
24
+ @meta_from_chunk ||= chunk[:meta]
25
+ end
26
+
27
+ def stop_process?
28
+ @stop_process ||= false
29
+ end
30
+
31
+ def stop_process!
32
+ @stop_process = true
33
+ end
34
+
35
+ private
36
+
37
+ def process_action!
38
+ return if stop_process?
39
+
40
+ action_caller = Logux::ActionCaller.new(
41
+ action: action_from_chunk,
42
+ meta: meta_from_chunk
43
+ )
44
+
45
+ stream.write(action_caller.call!.format)
46
+ end
47
+
48
+ def process_authorization!
49
+ policy_caller = Logux::PolicyCaller.new(action: action_from_chunk,
50
+ meta: meta_from_chunk)
51
+ policy_check = policy_caller.call!
52
+ status = policy_check ? :approved : :forbidden
53
+ stream.write([status, meta_from_chunk.id])
54
+ return stream.write(',') if policy_check
55
+
56
+ stop_process!
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Logux
4
+ module Process
5
+ class Auth
6
+ attr_reader :stream, :chunk
7
+
8
+ def initialize(stream:, chunk:)
9
+ @stream = stream
10
+ @chunk = chunk
11
+ end
12
+
13
+ def call
14
+ stream.write([auth_result, chunk.auth_id])
15
+ end
16
+
17
+ AUTHENTICATED = 'authenticated'
18
+ DENIED = 'denied'
19
+
20
+ private
21
+
22
+ def auth_result
23
+ auth_rule(chunk.user_id, chunk.credentials) ? AUTHENTICATED : DENIED
24
+ end
25
+
26
+ def auth_rule(user_id, credentials)
27
+ Logux.configuration.auth_rule.call(user_id, credentials)
28
+ end
29
+ end
30
+ end
31
+ end