thron 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.env.example +4 -0
- data/.gitignore +13 -0
- data/.travis.yml +6 -0
- data/Gemfile +4 -0
- data/README.md +182 -0
- data/Rakefile +10 -0
- data/Vagrantfile +80 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/config/thron.yml +10 -0
- data/lib/thron.rb +3 -0
- data/lib/thron/circuit_breaker.rb +46 -0
- data/lib/thron/config.rb +35 -0
- data/lib/thron/entity/base.rb +78 -0
- data/lib/thron/entity/image.rb +30 -0
- data/lib/thron/gateway/access_manager.rb +69 -0
- data/lib/thron/gateway/apps.rb +91 -0
- data/lib/thron/gateway/apps_admin.rb +153 -0
- data/lib/thron/gateway/base.rb +41 -0
- data/lib/thron/gateway/category.rb +210 -0
- data/lib/thron/gateway/client.rb +59 -0
- data/lib/thron/gateway/comment.rb +76 -0
- data/lib/thron/gateway/contact.rb +120 -0
- data/lib/thron/gateway/content.rb +267 -0
- data/lib/thron/gateway/content_category.rb +32 -0
- data/lib/thron/gateway/content_list.rb +40 -0
- data/lib/thron/gateway/dashboard.rb +120 -0
- data/lib/thron/gateway/delivery.rb +122 -0
- data/lib/thron/gateway/device.rb +58 -0
- data/lib/thron/gateway/metadata.rb +68 -0
- data/lib/thron/gateway/publish_in_weebo_express.rb +39 -0
- data/lib/thron/gateway/publishing_process.rb +141 -0
- data/lib/thron/gateway/repository.rb +111 -0
- data/lib/thron/gateway/session.rb +15 -0
- data/lib/thron/gateway/users_group_manager.rb +117 -0
- data/lib/thron/gateway/v_user_manager.rb +195 -0
- data/lib/thron/logger.rb +25 -0
- data/lib/thron/pageable.rb +26 -0
- data/lib/thron/paginator.rb +82 -0
- data/lib/thron/response.rb +48 -0
- data/lib/thron/root.rb +9 -0
- data/lib/thron/routable.rb +80 -0
- data/lib/thron/route.rb +102 -0
- data/lib/thron/string_extensions.rb +23 -0
- data/lib/thron/user.rb +77 -0
- data/lib/thron/version.rb +3 -0
- data/log/.gitignore +4 -0
- data/thron.gemspec +26 -0
- metadata +176 -0
data/lib/thron/logger.rb
ADDED
@@ -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
|
data/lib/thron/root.rb
ADDED
@@ -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
|
data/lib/thron/route.rb
ADDED
@@ -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
|
data/lib/thron/user.rb
ADDED
@@ -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
|