thron 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.env.example +4 -0
  3. data/.gitignore +13 -0
  4. data/.travis.yml +6 -0
  5. data/Gemfile +4 -0
  6. data/README.md +182 -0
  7. data/Rakefile +10 -0
  8. data/Vagrantfile +80 -0
  9. data/bin/console +14 -0
  10. data/bin/setup +7 -0
  11. data/config/thron.yml +10 -0
  12. data/lib/thron.rb +3 -0
  13. data/lib/thron/circuit_breaker.rb +46 -0
  14. data/lib/thron/config.rb +35 -0
  15. data/lib/thron/entity/base.rb +78 -0
  16. data/lib/thron/entity/image.rb +30 -0
  17. data/lib/thron/gateway/access_manager.rb +69 -0
  18. data/lib/thron/gateway/apps.rb +91 -0
  19. data/lib/thron/gateway/apps_admin.rb +153 -0
  20. data/lib/thron/gateway/base.rb +41 -0
  21. data/lib/thron/gateway/category.rb +210 -0
  22. data/lib/thron/gateway/client.rb +59 -0
  23. data/lib/thron/gateway/comment.rb +76 -0
  24. data/lib/thron/gateway/contact.rb +120 -0
  25. data/lib/thron/gateway/content.rb +267 -0
  26. data/lib/thron/gateway/content_category.rb +32 -0
  27. data/lib/thron/gateway/content_list.rb +40 -0
  28. data/lib/thron/gateway/dashboard.rb +120 -0
  29. data/lib/thron/gateway/delivery.rb +122 -0
  30. data/lib/thron/gateway/device.rb +58 -0
  31. data/lib/thron/gateway/metadata.rb +68 -0
  32. data/lib/thron/gateway/publish_in_weebo_express.rb +39 -0
  33. data/lib/thron/gateway/publishing_process.rb +141 -0
  34. data/lib/thron/gateway/repository.rb +111 -0
  35. data/lib/thron/gateway/session.rb +15 -0
  36. data/lib/thron/gateway/users_group_manager.rb +117 -0
  37. data/lib/thron/gateway/v_user_manager.rb +195 -0
  38. data/lib/thron/logger.rb +25 -0
  39. data/lib/thron/pageable.rb +26 -0
  40. data/lib/thron/paginator.rb +82 -0
  41. data/lib/thron/response.rb +48 -0
  42. data/lib/thron/root.rb +9 -0
  43. data/lib/thron/routable.rb +80 -0
  44. data/lib/thron/route.rb +102 -0
  45. data/lib/thron/string_extensions.rb +23 -0
  46. data/lib/thron/user.rb +77 -0
  47. data/lib/thron/version.rb +3 -0
  48. data/log/.gitignore +4 -0
  49. data/thron.gemspec +26 -0
  50. metadata +176 -0
@@ -0,0 +1,25 @@
1
+ require 'logger'
2
+ require 'thron/config'
3
+
4
+ module Thron
5
+ extend self
6
+
7
+ LOGGER_FILE = Thron::root.join('log', 'thron.log')
8
+ LOGGER_LEVELS = %i[debug info warn error fatal unknown]
9
+
10
+ def logger_level
11
+ LOGGER_LEVELS.fetch(logger.level)
12
+ end
13
+
14
+ def logger(options = {})
15
+ file = options.fetch(:file) { LOGGER_FILE }
16
+ level = options.fetch(:level) { Config::logger::level }
17
+ @logger ||= Logger.new(file).tap do |logger|
18
+ logger.level = level
19
+ end
20
+ end
21
+
22
+ def reset_logger(logger = Logger.new(STDOUT))
23
+ @logger = logger
24
+ end
25
+ end
@@ -0,0 +1,26 @@
1
+ require 'thron/paginator'
2
+
3
+ module Thron
4
+ module Pageable
5
+ module ClassMethods
6
+ def paginate(*apis)
7
+ (@paginated_apis = apis).each do |api|
8
+ define_method("#{api}_paginator") do |*args|
9
+ options = args.empty? ? {} : args.last
10
+ limit = options.delete(:limit) { Paginator::MAX_LIMIT }
11
+ body = ->(limit, offset) { send(api, options.merge!({ offset: offset, limit: limit })) }
12
+ Paginator::new(body: body, limit: limit)
13
+ end
14
+ end
15
+ end
16
+
17
+ def paginator_methods
18
+ Array(@paginated_apis).map { |api| :"#{api}_paginator" }
19
+ end
20
+ end
21
+
22
+ def self.included(klass)
23
+ klass.extend ClassMethods
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,82 @@
1
+ require 'ostruct'
2
+ require 'thron/response'
3
+
4
+ module Thron
5
+ class Paginator
6
+ MAX_LIMIT = 50
7
+
8
+ def self.check_limit(limit)
9
+ limit.to_i.tap do |limit|
10
+ return MAX_LIMIT if limit > MAX_LIMIT
11
+ end
12
+ end
13
+
14
+ attr_reader :offset, :limit, :cache
15
+
16
+ def initialize(options = {})
17
+ body = options[:body]
18
+ limit = options.fetch(:limit) { MAX_LIMIT }
19
+ fail ArgumentError, 'body must be a proc object' unless body.is_a?(Proc)
20
+ fail ArgumentError, 'body must accept the limit and offset attributes' unless body.arity == 2
21
+ @body = body
22
+ @limit = self.class.check_limit(limit)
23
+ @offset = offset.to_i
24
+ @cache = {}
25
+ end
26
+
27
+ def prev
28
+ @offset = prev_offset
29
+ fetch.value
30
+ end
31
+
32
+ def next
33
+ @offset = next_offset
34
+ fetch.value
35
+ end
36
+
37
+ def preload(n)
38
+ starting_offset = max_offset
39
+ (n).to_i.times do |i|
40
+ index = starting_offset.zero? ? i : (i + 1)
41
+ offset = starting_offset + (index * @limit)
42
+ fetch(offset)
43
+ end
44
+ end
45
+
46
+ def total
47
+ return @total if @total
48
+ return 0 if cache.empty?
49
+ @total = cache.fetch(0).value.total
50
+ end
51
+
52
+ private
53
+
54
+ def fetch(offset = @offset)
55
+ @cache.fetch(offset) do
56
+ call(offset).tap do |raw|
57
+ @cache[offset] = raw
58
+ end
59
+ end
60
+ end
61
+
62
+ def call(offset)
63
+ Thread::new { @body.call(@limit, offset) }
64
+ end
65
+
66
+ def next_offset
67
+ return 0 if cache.empty?
68
+ return @offset if total > 0 && (@offset + @limit) >= total
69
+ @offset + @limit
70
+ end
71
+
72
+ def prev_offset
73
+ return 0 if @offset <= @limit
74
+ @offset - @limit
75
+ end
76
+
77
+ def max_offset
78
+ return 0 if cache.empty?
79
+ @cache.max.first
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,48 @@
1
+ require 'thron/string_extensions'
2
+
3
+ module Thron
4
+ using StringExtensions
5
+ class Response
6
+ attr_accessor :body
7
+ attr_reader :http_code, :result_code, :sso_code, :total, :other_results, :error
8
+
9
+ ERROR_KEY = 'errorDescription'
10
+ ID_REGEX = /\A\w{8}-\w{4}-\w{4}-\w{4}-\w{12}\Z/
11
+
12
+ def initialize(raw_data)
13
+ @http_code = raw_data.code
14
+ @body = fetch(raw_data)
15
+ @result_code = @body.delete('resultCode')
16
+ @sso_code = @body.delete('ssoCode')
17
+ @total = @body.delete('totalResults')
18
+ @other_results = @body.delete('otherResults') { false }
19
+ @error = @body.delete(ERROR_KEY)
20
+ end
21
+
22
+ def extra(options = {})
23
+ attribute = options[:attribute].to_s
24
+ name = attribute.snakecase
25
+ self.class.send(:attr_reader, name)
26
+ instance_variable_set(:"@#{name}", body.delete(attribute))
27
+ end
28
+
29
+ def is_200?
30
+ (@http_code.to_i / 100) == 2
31
+ end
32
+
33
+ private
34
+
35
+ def fetch(raw_data)
36
+ case(parsed = raw_data.parsed_response)
37
+ when Hash
38
+ parsed
39
+ when ID_REGEX
40
+ { id: parsed }
41
+ when String
42
+ { ERROR_KEY => parsed }
43
+ else
44
+ {}
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,9 @@
1
+ require 'pathname'
2
+
3
+ module Thron
4
+ extend self
5
+
6
+ def root
7
+ @root ||= Pathname.new(File.expand_path(File.join('..', '..', '..'), __FILE__))
8
+ end
9
+ end
@@ -0,0 +1,80 @@
1
+ require 'httparty'
2
+ require 'thron/config'
3
+ require 'thron/route'
4
+ require 'thron/circuit_breaker'
5
+ require 'thron/response'
6
+ require 'thron/logger'
7
+
8
+ module Thron
9
+ module Routable
10
+ include HTTParty
11
+
12
+ class NoentRouteError < StandardError; end
13
+
14
+ def self.included(klass)
15
+ klass.extend ClassMethods
16
+ klass.class_eval do
17
+ include HTTParty
18
+ end
19
+ end
20
+
21
+ def self.info(host, query, body, route, token_id, dash)
22
+ info = [
23
+ "\n",
24
+ "*" * 50,
25
+ 'HTTP REQUEST:',
26
+ " * host: #{host}",
27
+ " * url: #{route.url}",
28
+ " * verb: #{route.verb.upcase}",
29
+ " * query: #{query.inspect}",
30
+ " * body: #{body.inspect}",
31
+ " * headers: #{route.headers(token_id: token_id, dash: dash)}",
32
+ "*" * 50,
33
+ "\n"
34
+ ]
35
+ puts info if Config::logger::verbose
36
+ Thron::logger.debug info.join("\n")
37
+ end
38
+
39
+ module ClassMethods
40
+ def circuit_breaker
41
+ @circuit_breaker ||= CircuitBreaker::new(threshold: Config::circuit_breaker.threshold)
42
+ end
43
+
44
+ def routes
45
+ fail NotImplementedError
46
+ end
47
+ end
48
+
49
+ def route(options = {})
50
+ to = options[:to]
51
+ query = options.fetch(:query) { {} }
52
+ body = options.fetch(:body) { {} }
53
+ token_id = options[:token_id]
54
+ dash = options.fetch(:dash) { true }
55
+ params = options[:params].to_a
56
+ route = fetch_route(to, params)
57
+ body = body.to_json if !body.empty? && route.json?
58
+ self.class.circuit_breaker.monitor do
59
+ raw = self.class.send(route.verb,
60
+ route.url,
61
+ { query: query,
62
+ body: body,
63
+ headers: route.headers(token_id: token_id, dash: dash) })
64
+ Routable::info(self.class.default_options[:base_uri], query, body, route, token_id, dash)
65
+ Response::new(raw).tap do |response|
66
+ yield(response) if response.is_200? && block_given?
67
+ end
68
+ end
69
+ rescue CircuitBreaker::OpenError
70
+ Thron::logger.error "Circuit breaker is open for process #{$$}"
71
+ Response::new(OpenStruct::new(code: 200))
72
+ end
73
+
74
+ private
75
+
76
+ def fetch_route(to, params)
77
+ self.class.routes.fetch(to) { fail NoentRouteError, "#{to} route does not exist!" }.call(params)
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,102 @@
1
+ module Thron
2
+ class Route
3
+ module Types
4
+ ALL = %w[json xml plain multipart]
5
+ ALL.each do |type|
6
+ const_set(type.upcase, type)
7
+ end
8
+ end
9
+
10
+ module Verbs
11
+ ALL = %w[post get put delete]
12
+ ALL.each do |type|
13
+ const_set(type.upcase, type)
14
+ end
15
+ end
16
+
17
+ class UnsupportedVerbError < StandardError; end
18
+ class UnsupportedTypeError < StandardError; end
19
+
20
+ def self.factory(options = {})
21
+ name = options[:name]
22
+ package = options[:package]
23
+ params = options[:params].to_a
24
+ verb = options.fetch(:verb) { Verbs::POST }
25
+ type = options.fetch(:type) { Types::JSON }
26
+ accept = options.fetch(:accept) { Types::JSON }
27
+ url = "/#{package}/#{name}"
28
+ url << "/#{params.join('/')}" unless params.empty?
29
+ Route::new(verb: verb, url: url, type: type, accept: accept)
30
+ end
31
+
32
+ def self.lazy_factory(options)
33
+ options.delete(:params)
34
+ ->(params) { factory(options.merge({ params: params })) }
35
+ end
36
+
37
+ def self.header_type(type)
38
+ case type.to_s
39
+ when Types::JSON
40
+ 'application/json'
41
+ when Types::XML
42
+ 'application/xml'
43
+ when Types::MULTIPART
44
+ 'multipart/form-data'
45
+ when Types::PLAIN
46
+ 'text/plain'
47
+ end
48
+ end
49
+
50
+ attr_reader :verb, :url
51
+
52
+ def initialize(options = {})
53
+ @verb = check_verb(options[:verb])
54
+ @url = options[:url]
55
+ @type = check_type(options[:type])
56
+ @accept = check_type(options[:accept])
57
+ end
58
+
59
+ def call(*args)
60
+ self
61
+ end
62
+
63
+ def json?
64
+ @type == Types::JSON
65
+ end
66
+
67
+ def format
68
+ return {} unless @format
69
+ { format: @format }
70
+ end
71
+
72
+ def headers(options = {})
73
+ @headers = {
74
+ 'Accept' => self.class.header_type(@accept),
75
+ content_type_key(options[:dash]) => self.class.header_type(@type)
76
+ }.tap do |headers|
77
+ headers.merge!({ 'X-TOKENID' => options[:token_id] }) if options[:token_id]
78
+ end
79
+ end
80
+
81
+ private
82
+
83
+ def content_type_key(dash = nil)
84
+ "Content_Type".tap do |key|
85
+ key.sub!('_', '-') if dash
86
+ end
87
+ end
88
+
89
+ def check_verb(verb)
90
+ verb.tap do |verb|
91
+ fail UnsupportedVerbError, "#{verb} is not supported" unless Verbs::ALL.include?(verb)
92
+ end
93
+ end
94
+
95
+ def check_type(type)
96
+ type.tap do |type|
97
+ fail UnsupportedTypeError, "#{type} is not supported" unless Types::ALL.include?(type)
98
+ end
99
+ end
100
+ end
101
+ end
102
+
@@ -0,0 +1,23 @@
1
+ module Thron
2
+ module StringExtensions
3
+ refine String do
4
+ def snakecase
5
+ partition(/[A-Z]{2,}\Z/).reject(&:empty?).reduce([]) do |acc, token|
6
+ token = token.gsub(/(.)([A-Z])/,'\1_\2').downcase unless token.uppercase?
7
+ acc << token
8
+ end.join('_')
9
+ end
10
+
11
+ def camelize_low
12
+ self.split('_').reduce([]) do |acc, token|
13
+ token.capitalize! unless acc.empty? || token.uppercase?
14
+ acc << token
15
+ end.join
16
+ end
17
+
18
+ def uppercase?
19
+ self.upcase == self
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,77 @@
1
+ require 'forwardable'
2
+ Dir[Thron.root.join('lib', 'thron', 'gateway', '*.rb')].each { |f| require f }
3
+
4
+ module Thron
5
+ class User
6
+ extend Forwardable
7
+
8
+ def_delegators :@access_gateway, *Gateway::AccessManager::routes.keys
9
+
10
+ def self.session_gateways
11
+ @session_gateways ||= Gateway::constants.select do |name|
12
+ Gateway.const_get(name) < Gateway::Session
13
+ end
14
+ end
15
+
16
+ def self.delegate_to_gateways
17
+ self.session_gateways.each do |name|
18
+ gateway = Gateway.const_get(name)
19
+ def_delegators "@gateways[:#{name}]", *(gateway.routes::keys + gateway.paginator_methods)
20
+ end
21
+ end
22
+
23
+ delegate_to_gateways
24
+
25
+ attr_reader :token_id, :gateways
26
+
27
+ def initialize
28
+ @access_gateway = Gateway::AccessManager::new
29
+ end
30
+
31
+ def login(options)
32
+ @access_gateway.login(options).tap do |response|
33
+ @token_id = @access_gateway.token_id
34
+ refresh_gateways
35
+ end
36
+ end
37
+
38
+ def logout
39
+ return unless logged?
40
+ @token_id = @access_gateway.token_id = nil
41
+ @gateways = nil
42
+ end
43
+
44
+ def disguise(options)
45
+ response = su(options)
46
+ response.body[:id].tap do |token_id|
47
+ return response.error unless token_id
48
+ original_token, @token_id = @token_id, token_id
49
+ refresh_gateways
50
+ yield if block_given?
51
+ @token_id = original_token
52
+ refresh_gateways
53
+ end
54
+ end
55
+
56
+ def logged?
57
+ !!@token_id
58
+ end
59
+
60
+ private
61
+
62
+ def initialize_gateways
63
+ self.class.session_gateways.reduce({}) do |acc, name|
64
+ acc[name] = Gateway.const_get(name)::new(token_id: @token_id); acc
65
+ end
66
+ end
67
+
68
+ def refresh_gateways
69
+ return unless logged?
70
+ return (@gateways = initialize_gateways) unless @gateways
71
+ @access_gateway.token_id = @token_id
72
+ @gateways.each do |name, gateway|
73
+ gateway.token_id = @token_id
74
+ end
75
+ end
76
+ end
77
+ end