gin 1.1.2 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.rdoc +22 -1
- data/Manifest.txt +7 -0
- data/TODO.rdoc +45 -0
- data/bin/gin +105 -35
- data/lib/gin.rb +7 -1
- data/lib/gin/app.rb +328 -59
- data/lib/gin/asset_manifest.rb +178 -0
- data/lib/gin/asset_pipeline.rb +235 -0
- data/lib/gin/cache.rb +36 -0
- data/lib/gin/config.rb +3 -1
- data/lib/gin/constants.rb +6 -1
- data/lib/gin/controller.rb +180 -17
- data/lib/gin/core_ext/float.rb +10 -0
- data/lib/gin/core_ext/time.rb +41 -0
- data/lib/gin/filterable.rb +5 -5
- data/lib/gin/mountable.rb +100 -0
- data/lib/gin/request.rb +4 -12
- data/lib/gin/response.rb +4 -2
- data/lib/gin/router.rb +110 -37
- data/lib/gin/strict_hash.rb +33 -0
- data/lib/gin/test.rb +8 -4
- data/lib/gin/worker.rb +49 -0
- data/test/mock_app.rb +7 -7
- data/test/test_app.rb +266 -17
- data/test/test_cache.rb +73 -5
- data/test/test_config.rb +4 -4
- data/test/test_controller.rb +158 -32
- data/test/test_filterable.rb +16 -1
- data/test/test_gin.rb +7 -6
- data/test/test_request.rb +6 -1
- data/test/test_response.rb +1 -1
- data/test/test_router.rb +156 -34
- data/test/test_test.rb +51 -45
- metadata +42 -14
- checksums.yaml +0 -7
@@ -0,0 +1,178 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'digest/md5'
|
3
|
+
|
4
|
+
class Gin::AssetManifest
|
5
|
+
|
6
|
+
class Asset
|
7
|
+
|
8
|
+
attr_reader :path, :mtime, :digest, :dependencies
|
9
|
+
attr_accessor :target_file
|
10
|
+
|
11
|
+
def initialize path, opts={}
|
12
|
+
@path = path
|
13
|
+
@rtime = opts[:rtime] || Time.now
|
14
|
+
@mtime = opts[:mtime] || File.mtime(path)
|
15
|
+
@digest = opts[:digest] || Digest::MD5.file(path).hexdigest
|
16
|
+
@target_file = opts[:target_file]
|
17
|
+
@dependencies = opts[:dependencies] || []
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
def update!
|
22
|
+
return unless File.file?(@path)
|
23
|
+
@rtime = Time.now
|
24
|
+
@mtime = File.mtime(@path)
|
25
|
+
@digest = Digest::MD5.file(@path).hexdigest
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
def outdated?
|
30
|
+
return true if !File.file?(@path)
|
31
|
+
return true if @target_file && !File.file?(@target_file)
|
32
|
+
return @mtime != File.mtime(@path) if @rtime - @mtime > 0
|
33
|
+
@digest != Digest::MD5.file(@path).hexdigest
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
def to_hash
|
38
|
+
hash = {
|
39
|
+
:target_file => @target_file,
|
40
|
+
:rtime => @rtime,
|
41
|
+
:mtime => @mtime,
|
42
|
+
:digest => @digest }
|
43
|
+
|
44
|
+
hash[:dependencies] = @dependencies.dup unless @dependencies.empty?
|
45
|
+
|
46
|
+
hash
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
attr_reader :assets, :filepath
|
52
|
+
attr_accessor :render_dir, :asset_globs
|
53
|
+
|
54
|
+
|
55
|
+
def initialize filepath, render_dir, asset_globs
|
56
|
+
@staged = []
|
57
|
+
@assets = {}
|
58
|
+
@filepath = filepath
|
59
|
+
@render_dir = render_dir
|
60
|
+
@asset_globs = asset_globs
|
61
|
+
|
62
|
+
load_file! if File.file?(@filepath)
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
def set asset_file, target_file, dependencies=[]
|
67
|
+
squash_similar_targets(target_file)
|
68
|
+
|
69
|
+
asset = @assets[asset_file] =
|
70
|
+
Asset.new(asset_file, :target_file => target_file)
|
71
|
+
|
72
|
+
Array(dependencies).each do |path|
|
73
|
+
@assets[path] ||= Asset.new(path)
|
74
|
+
asset.dependencies << path
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
def squash_similar_targets target_file
|
80
|
+
matcher = Regexp.escape( target_file.sub(/-\w+\.(\w+)$/, '') )
|
81
|
+
matcher = Regexp.new "#{matcher}-\\w+\\.#{$1}"
|
82
|
+
|
83
|
+
@assets.each do |name, asset|
|
84
|
+
if asset.target_file && matcher =~ asset.target_file
|
85
|
+
asset.target_file = nil
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
def stage asset_file, target_file, dependencies=[]
|
92
|
+
@staged << [asset_file, target_file, dependencies]
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
def delete asset_file
|
97
|
+
@assets.delete asset_file.to_s
|
98
|
+
end
|
99
|
+
|
100
|
+
|
101
|
+
def commit!
|
102
|
+
until @staged.empty?
|
103
|
+
set(*@staged.shift)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
def outdated?
|
109
|
+
source_changed? || @assets.keys.any?{|f| asset_outdated?(f) }
|
110
|
+
end
|
111
|
+
|
112
|
+
|
113
|
+
def asset_outdated? asset_file, checked=[]
|
114
|
+
# Check for circular dependencies
|
115
|
+
return false if checked.include?(asset_file)
|
116
|
+
checked << asset_file
|
117
|
+
|
118
|
+
asset = @assets[asset_file]
|
119
|
+
return true if !asset ||
|
120
|
+
asset.target_file && !asset.target_file.start_with?(@render_dir) ||
|
121
|
+
asset.outdated?
|
122
|
+
|
123
|
+
asset.dependencies.any? do |path|
|
124
|
+
return true if asset_outdated?(path, checked)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
|
129
|
+
def source_changed?
|
130
|
+
source_files.sort != @assets.keys.sort
|
131
|
+
end
|
132
|
+
|
133
|
+
|
134
|
+
def source_files
|
135
|
+
globs = @asset_globs.map{|gl|
|
136
|
+
(gl =~ /\.(\*|\w+)$/) ? gl : File.join(gl, '**', '*') }
|
137
|
+
|
138
|
+
Dir.glob(globs).reject{|path| !File.file?(path) }.uniq
|
139
|
+
end
|
140
|
+
|
141
|
+
|
142
|
+
def source_dirs
|
143
|
+
Dir.glob(@asset_globs).map{|pa| pa[-1] == ?/ ? pa[0..-2] : pa }.uniq
|
144
|
+
end
|
145
|
+
|
146
|
+
|
147
|
+
def to_hash
|
148
|
+
assets_hash = {}
|
149
|
+
|
150
|
+
@assets.each do |path, asset|
|
151
|
+
assets_hash[path] = asset.to_hash
|
152
|
+
end
|
153
|
+
|
154
|
+
assets_hash
|
155
|
+
end
|
156
|
+
|
157
|
+
|
158
|
+
def filepath= new_file
|
159
|
+
FileUtils.mv(@filepath, new_file) if
|
160
|
+
@filepath && new_file != @filepath && File.file?(@filepath)
|
161
|
+
@filepath = new_file
|
162
|
+
end
|
163
|
+
|
164
|
+
|
165
|
+
def save_file!
|
166
|
+
File.open(@filepath, 'w'){|f| f.write self.to_hash.to_yaml }
|
167
|
+
end
|
168
|
+
|
169
|
+
|
170
|
+
def load_file!
|
171
|
+
yaml = YAML.load_file(@filepath)
|
172
|
+
@assets.clear
|
173
|
+
|
174
|
+
yaml.each do |path, info|
|
175
|
+
@assets[path] = Asset.new path, info
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
@@ -0,0 +1,235 @@
|
|
1
|
+
require 'sprockets'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'gin/asset_manifest'
|
4
|
+
|
5
|
+
|
6
|
+
class Gin::AssetPipeline
|
7
|
+
|
8
|
+
attr_accessor :logger
|
9
|
+
|
10
|
+
def initialize manifest_file, render_dir, asset_paths, sprockets, &block
|
11
|
+
@rendering = 0
|
12
|
+
@logger = $stderr
|
13
|
+
@listen = false
|
14
|
+
@thread = false
|
15
|
+
@sprockets = nil
|
16
|
+
@flag_update = false
|
17
|
+
|
18
|
+
@render_lock = Gin::RWLock.new
|
19
|
+
@listen_lock = Gin::RWLock.new
|
20
|
+
|
21
|
+
@manifest = Gin::AssetManifest.new(manifest_file, render_dir, asset_paths)
|
22
|
+
|
23
|
+
setup_listener(asset_paths, sprockets, &block)
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
def setup_listener asset_paths=[], spr=nil, &block
|
28
|
+
spr = Sprockets::Environment.new unless Sprockets::Environment === spr
|
29
|
+
|
30
|
+
@manifest.asset_globs = asset_paths
|
31
|
+
|
32
|
+
@manifest.source_dirs.each do |path|
|
33
|
+
spr.append_path path
|
34
|
+
end
|
35
|
+
|
36
|
+
yield spr if block_given?
|
37
|
+
|
38
|
+
@manifest.asset_globs |= spr.paths
|
39
|
+
@sprockets = spr
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
def manifest_file
|
44
|
+
@manifest.filepath
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
def manifest_file= new_file
|
49
|
+
@manifest.filepath = new_file
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
def render_dir
|
54
|
+
@manifest.render_dir
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
def render_dir= new_dir
|
59
|
+
new_dir = File.expand_path(new_dir)
|
60
|
+
|
61
|
+
if @manifest.render_dir != new_dir
|
62
|
+
@listen_lock.write_sync do
|
63
|
+
# TODO: instead of re-rendering everything, maybe move rendered assets?
|
64
|
+
@flag_update = true
|
65
|
+
@manifest.render_dir = new_dir
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
def log str
|
72
|
+
@logger << "#{str}\n"
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
##
|
77
|
+
# Returns true if in the middle of rendering, otherwise false.
|
78
|
+
|
79
|
+
def rendering?
|
80
|
+
@render_lock.read_sync{ @rendering != 0 || @flag_update }
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
def listen
|
85
|
+
stop if listen?
|
86
|
+
|
87
|
+
@thread = Thread.new do
|
88
|
+
listen!
|
89
|
+
end
|
90
|
+
|
91
|
+
@thread.abort_on_exception = true
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
def listen!
|
96
|
+
@listen_lock.write_sync{ @listen = true }
|
97
|
+
|
98
|
+
while listen? do
|
99
|
+
render_all if outdated?
|
100
|
+
sleep 0.2
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
def listen?
|
106
|
+
@listen_lock.read_sync{ @listen }
|
107
|
+
end
|
108
|
+
|
109
|
+
|
110
|
+
def stop
|
111
|
+
@listen_lock.write_sync{ @listen = false }
|
112
|
+
end
|
113
|
+
|
114
|
+
|
115
|
+
def stop!
|
116
|
+
stop
|
117
|
+
@thread.join if @thread && @thread.alive?
|
118
|
+
end
|
119
|
+
|
120
|
+
|
121
|
+
def outdated?
|
122
|
+
@flag_update || @manifest.outdated?
|
123
|
+
end
|
124
|
+
|
125
|
+
|
126
|
+
def update_sprockets
|
127
|
+
paths = @manifest.source_dirs
|
128
|
+
return if @sprockets.paths == paths
|
129
|
+
|
130
|
+
@sprockets.clear_paths
|
131
|
+
paths.each{|path| @sprockets.append_path(path) }
|
132
|
+
end
|
133
|
+
|
134
|
+
|
135
|
+
def with_render_lock &block
|
136
|
+
@render_lock.write_sync{ @rendering += 1 }
|
137
|
+
start = Time.now
|
138
|
+
block.call
|
139
|
+
|
140
|
+
log "Assets rendered in (#{(Time.now.to_f - start.to_f).round(3)} sec)"
|
141
|
+
|
142
|
+
ensure
|
143
|
+
@render_lock.write_sync do
|
144
|
+
@rendering -= 1
|
145
|
+
@rendering = 0 if @rendering < 0
|
146
|
+
@flag_update = false
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
|
151
|
+
def remove_path path
|
152
|
+
log "Deleting asset: #{path}"
|
153
|
+
File.delete(path)
|
154
|
+
|
155
|
+
dir = File.dirname(path)
|
156
|
+
while dir != self.render_dir && Dir[File.join(dir,'*')].empty?
|
157
|
+
FileUtils.rm_r(dir)
|
158
|
+
dir = File.dirname(dir)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
|
163
|
+
##
|
164
|
+
# Looks at all rendered, added, and modified assets and compiles those
|
165
|
+
# out of date or missing.
|
166
|
+
|
167
|
+
def render_all
|
168
|
+
update_sprockets
|
169
|
+
|
170
|
+
with_render_lock do
|
171
|
+
sp_files = @sprockets.each_file.map(&:to_s).uniq
|
172
|
+
|
173
|
+
# delete rendered files that aren't in the asset_dirs
|
174
|
+
@manifest.assets.each do |asset_file, asset|
|
175
|
+
valid_asset = sp_files.include?(asset_file)
|
176
|
+
next if valid_asset
|
177
|
+
|
178
|
+
target_file = asset.target_file
|
179
|
+
remove_path target_file if target_file
|
180
|
+
@manifest.delete(asset_file)
|
181
|
+
end
|
182
|
+
|
183
|
+
# render assets and update tree index
|
184
|
+
sp_files.each do |asset_file|
|
185
|
+
next unless @manifest.asset_outdated?(asset_file)
|
186
|
+
sp_asset = @sprockets[asset_file] # First time render
|
187
|
+
|
188
|
+
if target_file = render(asset_file)
|
189
|
+
@manifest.stage asset_file, target_file,
|
190
|
+
sp_asset.dependencies.map{|d| d.pathname.to_s }
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
@manifest.commit!
|
195
|
+
|
196
|
+
# save cache to disk
|
197
|
+
@manifest.save_file!
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
|
202
|
+
def render path
|
203
|
+
asset = @sprockets[path]
|
204
|
+
return unless asset
|
205
|
+
|
206
|
+
ext = render_ext(asset)
|
207
|
+
render_path = File.join(self.render_dir, asset.logical_path)
|
208
|
+
|
209
|
+
file_glob = render_path.sub(/(\.\w+)$/, "-*#{ext}")
|
210
|
+
file_name = Dir[file_glob].first
|
211
|
+
|
212
|
+
digest = asset.digest[0..7]
|
213
|
+
return file_name if file_name && file_name.include?(digest)
|
214
|
+
|
215
|
+
log "Rendering asset: #{path}"
|
216
|
+
render_filename = file_glob.sub('*', digest)
|
217
|
+
|
218
|
+
FileUtils.mkdir_p File.dirname(render_filename)
|
219
|
+
File.open(render_filename, 'wb'){|f| f.write asset.source }
|
220
|
+
|
221
|
+
File.delete(file_name) if file_name
|
222
|
+
|
223
|
+
return render_filename
|
224
|
+
end
|
225
|
+
|
226
|
+
|
227
|
+
private
|
228
|
+
|
229
|
+
def render_ext asset
|
230
|
+
path = asset.pathname.to_s
|
231
|
+
ctype = asset.content_type
|
232
|
+
ctype == 'application/octet-stream' ?
|
233
|
+
File.extname(path) : @sprockets.extension_for_mime_type(ctype)
|
234
|
+
end
|
235
|
+
end
|
data/lib/gin/cache.rb
CHANGED
@@ -11,6 +11,14 @@ class Gin::Cache
|
|
11
11
|
end
|
12
12
|
|
13
13
|
|
14
|
+
##
|
15
|
+
# Clear all cache entries.
|
16
|
+
|
17
|
+
def clear
|
18
|
+
@lock.write_sync{ @data.clear }
|
19
|
+
end
|
20
|
+
|
21
|
+
|
14
22
|
##
|
15
23
|
# Set the write timeout when waiting for reader thread locks.
|
16
24
|
# Defaults to 0.05 sec. See Gin::RWLock for more details.
|
@@ -45,6 +53,34 @@ class Gin::Cache
|
|
45
53
|
end
|
46
54
|
|
47
55
|
|
56
|
+
##
|
57
|
+
# Increases the value of the given key in a thread-safe manner.
|
58
|
+
# Only Numerics and nil values are supported.
|
59
|
+
# Treats nil or non-existant values as 0.
|
60
|
+
|
61
|
+
def increase key, amount=1
|
62
|
+
@lock.write_sync do
|
63
|
+
return unless @data[key].nil? || Numeric === @data[key]
|
64
|
+
@data[key] ||= 0
|
65
|
+
@data[key] += amount
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
##
|
71
|
+
# Decreases the value of the given key in a thread-safe manner.
|
72
|
+
# Only Numerics and nil values are supported.
|
73
|
+
# Treats nil or non-existant values as 0.
|
74
|
+
|
75
|
+
def decrease key, amount=1
|
76
|
+
@lock.write_sync do
|
77
|
+
return unless @data[key].nil? || Numeric === @data[key]
|
78
|
+
@data[key] ||= 0
|
79
|
+
@data[key] -= amount
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
|
48
84
|
##
|
49
85
|
# Check if the current key exists in the cache.
|
50
86
|
|