tango-etl 0.0.1
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/Gemfile +19 -0
- data/Rakefile +12 -0
- data/changelog.md +4 -0
- data/config/app.yml.sample +6 -0
- data/config/database.yml.sample +27 -0
- data/lib/tango/abstract_model.rb +53 -0
- data/lib/tango/app.rb +202 -0
- data/lib/tango/database_locker.rb +49 -0
- data/lib/tango/etl/dispatcher.rb +49 -0
- data/lib/tango/etl/handler_interface.rb +40 -0
- data/lib/tango/etl/operator_interface.rb +36 -0
- data/lib/tango/etl.rb +3 -0
- data/lib/tango/kernel.rb +36 -0
- data/lib/tango/link_stack.rb +61 -0
- data/lib/tango/multidb.rb +11 -0
- data/lib/tango/resource/buffer.rb +74 -0
- data/lib/tango/resource/cache.rb +81 -0
- data/lib/tango/resource.rb +2 -0
- data/lib/tango/version.rb +3 -0
- data/lib/tango.rb +18 -0
- data/readme.md +3 -0
- data/tango.gemspec +20 -0
- data/test/support/db/schema.rb +6 -0
- data/test/support/lib/model/user.rb +11 -0
- data/test/support/lib/simple_buffer.rb +18 -0
- data/test/support/lib/simple_handler.rb +18 -0
- data/test/unit/etl/test_dispatcher.rb +22 -0
- data/test/unit/resource/test_buffer.rb +51 -0
- data/test/unit/resource/test_cache.rb +120 -0
- data/test/unit/test_abstract_model.rb +43 -0
- data/test/unit/test_database_locker.rb +32 -0
- data/test/unit/test_kernel.rb +35 -0
- data/test/unit/test_link_stack.rb +49 -0
- metadata +177 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0ca6376b79e444981adcf8ee8c66f4cd2a67d6d5
|
4
|
+
data.tar.gz: 0e567db2d98e6e1d7fc70c308e26d0959ab82541
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8d51e56712c4aecaca7bd5d1b67e9fad5b5846ed888d83a4cbc6265364c0e7f69dcf8a7f2cb0513bad4324c6ac003125ed7e56a0416f5cf9cd543a1c1a6b96eb
|
7
|
+
data.tar.gz: 7ec825c8bc994fd1d0d765d6345e3c39f8fb2f75bc4559663816ebd0cf5aefe0be6df2ec7ee397ba186336bd0810577500bc64d6e3292cbd828735f35c92ef50
|
data/Gemfile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
ruby '2.0.0'
|
4
|
+
|
5
|
+
gem 'nokogiri', '~> 1.6.1'
|
6
|
+
gem 'httparty', '~> 0.13.1'
|
7
|
+
gem 'activerecord', '~> 4.1.0'
|
8
|
+
gem 'activerecord-import', '~> 0.5.0'
|
9
|
+
gem 'ar-multidb', '~> 0.1.12'
|
10
|
+
|
11
|
+
group :development do
|
12
|
+
gem "yard", "~> 0.8.7"
|
13
|
+
end
|
14
|
+
|
15
|
+
group :test do
|
16
|
+
gem "shoulda", "~> 3.5.0"
|
17
|
+
gem "mocha", "~> 1.0.0"
|
18
|
+
gem 'activerecord-nulldb-adapter', '~> 0.3.1'
|
19
|
+
end
|
data/Rakefile
ADDED
data/changelog.md
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
development:
|
2
|
+
adapter: mysql2
|
3
|
+
database: tango_dev_master
|
4
|
+
username: root
|
5
|
+
password: pass
|
6
|
+
host: localhost
|
7
|
+
multidb:
|
8
|
+
databases:
|
9
|
+
slave:
|
10
|
+
database: tango_dev_slave
|
11
|
+
production:
|
12
|
+
adapter: mysql2
|
13
|
+
database: tango_prod_master
|
14
|
+
username: root
|
15
|
+
password: pass
|
16
|
+
host: localhost
|
17
|
+
multidb:
|
18
|
+
databases:
|
19
|
+
slave:
|
20
|
+
database: tango_prod_slave
|
21
|
+
test:
|
22
|
+
adapter: mysql2
|
23
|
+
database: tango_test
|
24
|
+
username: root
|
25
|
+
password: pass
|
26
|
+
host: localhost
|
27
|
+
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Tango
|
2
|
+
|
3
|
+
# Base model for Tango resources
|
4
|
+
#
|
5
|
+
# @author Mckomo
|
6
|
+
class AbstractModel < ::ActiveRecord::Base
|
7
|
+
|
8
|
+
# Required by ActiveRecord
|
9
|
+
self.abstract_class = true
|
10
|
+
|
11
|
+
@properties = nil
|
12
|
+
@last_id = nil
|
13
|
+
|
14
|
+
# Return array with values of model properties
|
15
|
+
#
|
16
|
+
# @return [Array]
|
17
|
+
def values
|
18
|
+
self.attributes.values
|
19
|
+
end
|
20
|
+
|
21
|
+
# Return cache key of model instance
|
22
|
+
#
|
23
|
+
# @return [Object]
|
24
|
+
def cache_key
|
25
|
+
raise NotImplementedError
|
26
|
+
end
|
27
|
+
|
28
|
+
# Return array with names of model properties
|
29
|
+
#
|
30
|
+
# @return [Array]
|
31
|
+
def self.properties
|
32
|
+
@properties || @properties = self.attribute_names.map { |a| a.to_sym }
|
33
|
+
end
|
34
|
+
|
35
|
+
# Return incremented value of last id in the model's table
|
36
|
+
#
|
37
|
+
# @return [Integer]
|
38
|
+
def self.next_id
|
39
|
+
@last_id ||= self.pluck( :id ).last || 0
|
40
|
+
@last_id += 1
|
41
|
+
end
|
42
|
+
|
43
|
+
# State wether model should be cached or not
|
44
|
+
#
|
45
|
+
# @return [Boolean]
|
46
|
+
def self.persistent?
|
47
|
+
raise NotImplementedError
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
data/lib/tango/app.rb
ADDED
@@ -0,0 +1,202 @@
|
|
1
|
+
require 'tango/version'
|
2
|
+
|
3
|
+
module Tango
|
4
|
+
|
5
|
+
# Interface for Tango app runtime filters
|
6
|
+
#
|
7
|
+
# @author Mckomo
|
8
|
+
class App
|
9
|
+
|
10
|
+
attr_reader :config, :dispatcher, :link_stack, :logger
|
11
|
+
|
12
|
+
# @param link_stack [Tango::LinkStack]
|
13
|
+
# @param dispatcher [Tango::Etl::Dispatcher]
|
14
|
+
# @param cache [Tango::Resources::Cache]
|
15
|
+
# @param http_client [Object] Must implement get method
|
16
|
+
# @param parser [Object] Must implement parse method
|
17
|
+
# @param db_locker [DatabaseLocker]
|
18
|
+
# @param logger [Logger]
|
19
|
+
# @return [Tango::App]
|
20
|
+
def initialize( config: {}, link_stack: nil, dispatcher: nil, cache: nil, http_client: nil, parser: nil, db_locker: nil, logger: nil )
|
21
|
+
|
22
|
+
# Init app properties
|
23
|
+
@models = {}
|
24
|
+
@operators = {}
|
25
|
+
|
26
|
+
# Set config
|
27
|
+
@config = config
|
28
|
+
|
29
|
+
# Set dependencies
|
30
|
+
@link_stack = link_stack || LinkStack.new( config['target_url'] )
|
31
|
+
@dispatcher = dispatcher || ETL::Dispatcher.new
|
32
|
+
@cache = cache || Resource::Cache.new( Resource::Buffer.new )
|
33
|
+
@http_client = http_client || HTTParty
|
34
|
+
@parser = parser || Nokogiri::HTML
|
35
|
+
@db_locker = db_locker || DatabaseLocker.new( Multidb.databases )
|
36
|
+
@logger = logger || Logger.new( STDOUT )
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
# Filter run before Tango execution
|
41
|
+
def before
|
42
|
+
raise NotImplementedError
|
43
|
+
end
|
44
|
+
|
45
|
+
# Filter run after Tango execution
|
46
|
+
def after
|
47
|
+
raise NotImplementedError
|
48
|
+
end
|
49
|
+
|
50
|
+
# Register new resource model
|
51
|
+
#
|
52
|
+
# @param symbol [Symbol]
|
53
|
+
# @param model [Class]
|
54
|
+
def register_model( symbol, model )
|
55
|
+
|
56
|
+
@models[symbol] = model
|
57
|
+
|
58
|
+
# Truncate table of non persistent model
|
59
|
+
unless model.persistent?
|
60
|
+
ActiveRecord::Base.connection.execute( "TRUNCATE #{model.table_name}" )
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
# Register new resource operator
|
66
|
+
#
|
67
|
+
# @param symbol [Symbol]
|
68
|
+
# @param operator [Class]
|
69
|
+
def register_operator( symbol, operator )
|
70
|
+
|
71
|
+
@operators[symbol] = operator
|
72
|
+
|
73
|
+
# Register operator with resource cache system
|
74
|
+
@cache.register( symbol ) do |resource|
|
75
|
+
operator.load( resource )
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
# Run ETL process
|
81
|
+
#
|
82
|
+
# @param link_stack [Tango::LinkStack]
|
83
|
+
# @param dispatcher [Tango::Etl::Dispatcher]
|
84
|
+
# @param cache [Tango::Resources::Cache]
|
85
|
+
# @param http_client [Object] Must implement get method
|
86
|
+
# @param parser [Object] Must implement parse method
|
87
|
+
# @param logger [Logger]
|
88
|
+
# @return [Integer]
|
89
|
+
def run
|
90
|
+
|
91
|
+
# Save beginning time
|
92
|
+
start_time = Time.now
|
93
|
+
|
94
|
+
@logger.info "Running Tango v.#{Tango::VERSION} ..."
|
95
|
+
@logger.info "Target: #{@link_stack.host}."
|
96
|
+
|
97
|
+
# Use next unlocked database
|
98
|
+
Multidb.use( @db_locker.unlocked )
|
99
|
+
@logger.info "Using database '#{@db_locker.unlocked}'."
|
100
|
+
|
101
|
+
# Run before filter
|
102
|
+
@logger.info "Loading cache ..."
|
103
|
+
load_cache
|
104
|
+
|
105
|
+
# Run before filter
|
106
|
+
@logger.info "Running before callback ..."
|
107
|
+
before
|
108
|
+
|
109
|
+
# Init counter of crawled links
|
110
|
+
links_counter = 0
|
111
|
+
@logger.info "Tango starts crawling ..."
|
112
|
+
|
113
|
+
# Start crawling website
|
114
|
+
while( @link_stack.has_links? )
|
115
|
+
|
116
|
+
# Get a link from the stack
|
117
|
+
link = @link_stack.shift
|
118
|
+
|
119
|
+
# Skip iteration if no handler found
|
120
|
+
if ! handler_klass = @dispatcher.find_handler( link )
|
121
|
+
@logger.error "No handler for link: #{link}."
|
122
|
+
next
|
123
|
+
end
|
124
|
+
|
125
|
+
# Try to get contents of the link
|
126
|
+
begin
|
127
|
+
response = @http_client.get( @link_stack.host + link )
|
128
|
+
rescue StandardError => e
|
129
|
+
@logger.error "Could not download contents of #{@link_stack.host + link} link."; @logger.error e.message
|
130
|
+
next
|
131
|
+
end
|
132
|
+
|
133
|
+
# Continue only when response has code 200 or 201
|
134
|
+
if ! [ 200, 201 ].include?( response.code )
|
135
|
+
@logger.error "Response code for link #{link} is #{response.code}. Only code 200 is accepted."
|
136
|
+
next
|
137
|
+
end
|
138
|
+
|
139
|
+
# Use Nokogiri to parse response contents
|
140
|
+
document = @parser.parse( response.body )
|
141
|
+
|
142
|
+
# Init handler
|
143
|
+
handler = handler_klass.new( link, document, @cache )
|
144
|
+
|
145
|
+
# Append links fetched from hanlder
|
146
|
+
@link_stack.append( handler.links )
|
147
|
+
|
148
|
+
# Try to fire the handler
|
149
|
+
begin
|
150
|
+
handler.trigger
|
151
|
+
rescue StandardError => e
|
152
|
+
# Log error
|
153
|
+
@logger.error "Link: #{link}. Handler had some troubles."
|
154
|
+
@logger.error e.message
|
155
|
+
@logger.error e.backtrace.join( "\n" )
|
156
|
+
else
|
157
|
+
links_counter += 1
|
158
|
+
@logger.debug "Link: #{link}. Handler triggered successfully."
|
159
|
+
end
|
160
|
+
|
161
|
+
# Sleep to give crawled server time to breath
|
162
|
+
sleep( @config["sleep"] || 0 )
|
163
|
+
|
164
|
+
end
|
165
|
+
|
166
|
+
# Release buffers
|
167
|
+
@logger.info "Releasing buffers ..."
|
168
|
+
@cache.buffer.release_all()
|
169
|
+
|
170
|
+
# Run after filter
|
171
|
+
@logger.info "Running after callback ..."
|
172
|
+
after
|
173
|
+
|
174
|
+
# Lock database used in this Tango iteration
|
175
|
+
@db_locker.lock( @db_locker.unlocked )
|
176
|
+
|
177
|
+
# Get time of script execution ending
|
178
|
+
end_time = Time.now
|
179
|
+
|
180
|
+
@logger.info "Tango crawled #{links_counter}/#{@link_stack.shifted} links successfully."
|
181
|
+
@logger.info "Start time: #{start_time}, end time: #{end_time}, time elapsed: #{end_time - start_time} seconds."
|
182
|
+
|
183
|
+
# Close logger
|
184
|
+
@logger.close
|
185
|
+
|
186
|
+
end
|
187
|
+
|
188
|
+
private
|
189
|
+
|
190
|
+
def load_cache
|
191
|
+
|
192
|
+
@models.each do |symbol, model|
|
193
|
+
model.all.each do |m|
|
194
|
+
@cache.set( symbol, m )
|
195
|
+
end if model.persistent?
|
196
|
+
end
|
197
|
+
|
198
|
+
end
|
199
|
+
|
200
|
+
end
|
201
|
+
|
202
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Tango
|
2
|
+
|
3
|
+
class DatabaseLocker
|
4
|
+
|
5
|
+
attr_reader :lock_path
|
6
|
+
|
7
|
+
#
|
8
|
+
#
|
9
|
+
def initialize( candidates = [], lock_path = "./tmp/database.lock" )
|
10
|
+
@candidates = candidates
|
11
|
+
@lock_path = lock_path
|
12
|
+
end
|
13
|
+
|
14
|
+
# Return next unlocked database
|
15
|
+
def unlocked
|
16
|
+
@unlocked ||= find_unlocked
|
17
|
+
end
|
18
|
+
|
19
|
+
def lock( database )
|
20
|
+
|
21
|
+
@unlocked = nil
|
22
|
+
|
23
|
+
File.open( lock_path, "w" ) do |f|
|
24
|
+
f.write( database )
|
25
|
+
end
|
26
|
+
|
27
|
+
self
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def find_unlocked
|
34
|
+
|
35
|
+
lock = File.open( lock_path, 'a+' ) { |f| f.read.strip.gsub(/\s+/, ' ') }
|
36
|
+
|
37
|
+
# If some database was locked use next one
|
38
|
+
unless lock.empty? or ! @candidates.include?( lock )
|
39
|
+
@candidates.at( @candidates.index( lock ).next % @candidates.length )
|
40
|
+
# Otherwise return first one
|
41
|
+
else
|
42
|
+
@candidates.first
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Tango
|
2
|
+
module ETL
|
3
|
+
|
4
|
+
# Dispatcher of handlers
|
5
|
+
#
|
6
|
+
# @author Mckomo
|
7
|
+
class Dispatcher
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@handlers = []
|
11
|
+
end
|
12
|
+
|
13
|
+
# Register new handler
|
14
|
+
#
|
15
|
+
# @param handler_class [HandlerInterface] Class that implements HandlerInterface
|
16
|
+
# @return [Dispatcher]
|
17
|
+
def register( handler_class )
|
18
|
+
|
19
|
+
# handler must implement HandlerInterface
|
20
|
+
unless handler_class.ancestors.include? Tango::ETL::HandlerInterface
|
21
|
+
raise "Handler must implement HandlerInterface"
|
22
|
+
end
|
23
|
+
|
24
|
+
# Append handler to container
|
25
|
+
@handlers << handler_class
|
26
|
+
|
27
|
+
self # Chainabilty!
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
# Find first applicable handler
|
32
|
+
#
|
33
|
+
# @param url [String] URL of the page to be handled
|
34
|
+
# @return [HandlerInterface]
|
35
|
+
def find_handler( url )
|
36
|
+
|
37
|
+
# Iterate handlers to find first matching handler
|
38
|
+
@handlers.each do |h|
|
39
|
+
return h if h.applicable?( url )
|
40
|
+
end
|
41
|
+
|
42
|
+
nil
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Tango
|
2
|
+
|
3
|
+
module ETL
|
4
|
+
|
5
|
+
# Handler interface
|
6
|
+
#
|
7
|
+
# @author Mckomo
|
8
|
+
class HandlerInterface
|
9
|
+
|
10
|
+
# Constructor of Tango's handler
|
11
|
+
#
|
12
|
+
#
|
13
|
+
def initialize( link, document, cache = nil )
|
14
|
+
@link = link
|
15
|
+
@document = document
|
16
|
+
@cache = cache
|
17
|
+
end
|
18
|
+
|
19
|
+
#
|
20
|
+
#
|
21
|
+
# @return [Array|String]
|
22
|
+
def links
|
23
|
+
raise NotImplementedError
|
24
|
+
end
|
25
|
+
|
26
|
+
#
|
27
|
+
#
|
28
|
+
# @return [NilClass]
|
29
|
+
def trigger
|
30
|
+
raise NotImplementedError
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.applicable?( link )
|
34
|
+
raise NotImplementedError
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Tango
|
2
|
+
|
3
|
+
module ETL
|
4
|
+
|
5
|
+
# Interface of an operator that implements ETL pattern
|
6
|
+
#
|
7
|
+
# @author Mckomo
|
8
|
+
class OperatorInterface
|
9
|
+
|
10
|
+
# Extract resource params
|
11
|
+
#
|
12
|
+
# @param element [Object] Element from witch resources should be extracted
|
13
|
+
# @return [Object] Extracted resource or array with resources
|
14
|
+
def self.extract( element )
|
15
|
+
raise NotImplementedError
|
16
|
+
end
|
17
|
+
|
18
|
+
# Transform resource params to desired state
|
19
|
+
#
|
20
|
+
# @param resource [Object] Resource or array with resources
|
21
|
+
# @return [Object] Transformed resource or array with resources
|
22
|
+
def self.transform( resource )
|
23
|
+
raise NotImplementedError
|
24
|
+
end
|
25
|
+
|
26
|
+
# Load resources into a storage
|
27
|
+
#
|
28
|
+
# @param resources [Array] Batch of resources to load
|
29
|
+
def self.load( resources )
|
30
|
+
raise NotImplementedError
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
data/lib/tango/etl.rb
ADDED
data/lib/tango/kernel.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
module Tango
|
2
|
+
module Kernel
|
3
|
+
|
4
|
+
# Convert file path to class name
|
5
|
+
# @param file_path [String]
|
6
|
+
# @return [String]
|
7
|
+
def self.classify( file_path )
|
8
|
+
File.basename( file_path, ".*" ).split( "_" ).map { |w| w.capitalize }.join
|
9
|
+
end
|
10
|
+
|
11
|
+
# Load class from a file
|
12
|
+
#
|
13
|
+
# @param file [String]
|
14
|
+
# @param module_prefix [String]
|
15
|
+
# @return [Class]
|
16
|
+
def self.load( file, module_prefix = "" )
|
17
|
+
|
18
|
+
require file
|
19
|
+
|
20
|
+
class_name = Kernel.classify( file )
|
21
|
+
Kernel.const_get( "#{module_prefix}#{class_name}" )
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
# Obtain symbol of a class
|
26
|
+
# @example
|
27
|
+
# Tango::Kernel.symblize( A::B::SuperKlass ) #=> :super_klass
|
28
|
+
#
|
29
|
+
# @param klass [Class]
|
30
|
+
# @return [Symbol]
|
31
|
+
def self.symbolize( klass )
|
32
|
+
klass.to_s.split( '::' ).last.gsub( /(.)([A-Z])/ ,'\1_\2' ).downcase.to_sym
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# Load system lib
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
module Tango
|
5
|
+
|
6
|
+
# Stack of links to be crawled
|
7
|
+
#
|
8
|
+
# @author Mckomo
|
9
|
+
class LinkStack
|
10
|
+
|
11
|
+
attr_reader :host, :links, :shifted
|
12
|
+
|
13
|
+
def initialize( base_link )
|
14
|
+
|
15
|
+
if base_link !~ URI::regexp
|
16
|
+
raise ArgumentError, "'#{base_link}' is not valid website URL."
|
17
|
+
end
|
18
|
+
|
19
|
+
# Parse base link
|
20
|
+
url = URI( base_link )
|
21
|
+
|
22
|
+
@host = "#{url.scheme}://#{url.host}:#{url.port}" # Extract host from base link
|
23
|
+
@links = [] # Container for links (without host part)
|
24
|
+
@shifted = 0 # Shifted links counter
|
25
|
+
|
26
|
+
# Extract path (with query) from base link and append it as initial link
|
27
|
+
path = url.query ? "#{url.path}?#{url.query}" : url.path
|
28
|
+
append( path )
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
# Shift link from stack and get referrer content
|
33
|
+
#
|
34
|
+
# @return [String]
|
35
|
+
def shift
|
36
|
+
return unless has_links?
|
37
|
+
@shifted += 1
|
38
|
+
@links.shift
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
# Append link/s to stack
|
43
|
+
#
|
44
|
+
# @return [Array|String]
|
45
|
+
def append( links )
|
46
|
+
if links.is_a? String
|
47
|
+
@links << links
|
48
|
+
elsif links.is_a? Array
|
49
|
+
@links += links
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Check if link stack still has links
|
54
|
+
#
|
55
|
+
# @return [Boolean]
|
56
|
+
def has_links?
|
57
|
+
! @links.empty?
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Tango
|
2
|
+
module Resource
|
3
|
+
|
4
|
+
# Resource buffer
|
5
|
+
#
|
6
|
+
# @author Mckomo
|
7
|
+
class Buffer
|
8
|
+
|
9
|
+
# Constructor of the Buffer
|
10
|
+
#
|
11
|
+
# @param size [Integer]
|
12
|
+
# @return [Tango::Resources::Buffer]
|
13
|
+
def initialize( size = 500 )
|
14
|
+
|
15
|
+
# Set max size of the buffer
|
16
|
+
@size = size
|
17
|
+
|
18
|
+
# Init container for resources buffer
|
19
|
+
@resources = {}
|
20
|
+
# Init container for resource operators classes
|
21
|
+
@callbacks = {}
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
# Register new type of resource to be buffered
|
26
|
+
#
|
27
|
+
# @param type [Symbol]
|
28
|
+
# @param release_callback [Proc]
|
29
|
+
def register( type, &release_callback )
|
30
|
+
|
31
|
+
raise ArgumentError, "No release callback given" unless block_given?
|
32
|
+
|
33
|
+
@resources[type] = []
|
34
|
+
@callbacks[type] = release_callback
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
# Fill buffer with a resource
|
39
|
+
#
|
40
|
+
# @param type [Symbol]
|
41
|
+
# @param resource [Object]
|
42
|
+
def fill( type, resource )
|
43
|
+
|
44
|
+
raise ArgumentError, "Trying to fill object with unregistered type" unless @resources.keys.include?( type )
|
45
|
+
|
46
|
+
# Append resource to the buffer
|
47
|
+
@resources[type] << resource
|
48
|
+
# Release the buffer if buffer size exceeded
|
49
|
+
release( type ) if @resources[type].count >= @size
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
# Release all registered buffers
|
54
|
+
def release_all
|
55
|
+
@resources.keys.each { |type| release( type ) }
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
# Release buffer with given type
|
61
|
+
#
|
62
|
+
# @param type [Symbol]
|
63
|
+
def release( type )
|
64
|
+
# Trigger callback on released resources
|
65
|
+
@callbacks[type].tap do |c|
|
66
|
+
c.call( @resources[type] )
|
67
|
+
end
|
68
|
+
@resources[type].clear # Clear resources from the buffer
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|