gin 1.1.2 → 1.2.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.
- 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
|
|