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,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