thron 0.7.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 (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