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