mbj-assets 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/.rspec +1 -0
- data/.travis.yml +10 -0
- data/Gemfile +6 -0
- data/Gemfile.devtools +60 -0
- data/Guardfile +18 -0
- data/Rakefile +2 -0
- data/TODO +2 -0
- data/assets.gemspec +28 -0
- data/config/flay.yml +3 -0
- data/config/flog.yml +2 -0
- data/config/mutant.yml +3 -0
- data/config/reek.yml +93 -0
- data/config/roodi.yml +26 -0
- data/config/yardstick.yml +2 -0
- data/lib/assets/asset.rb +24 -0
- data/lib/assets/builder.rb +88 -0
- data/lib/assets/environment/cache.rb +113 -0
- data/lib/assets/environment/dynamic.rb +56 -0
- data/lib/assets/environment/static.rb +37 -0
- data/lib/assets/environment.rb +34 -0
- data/lib/assets/evaluator.rb +67 -0
- data/lib/assets/handler.rb +39 -0
- data/lib/assets/mime.rb +81 -0
- data/lib/assets/package.rb +124 -0
- data/lib/assets/repository.rb +61 -0
- data/lib/assets/responder.rb +133 -0
- data/lib/assets/rule/compile/css.rb +28 -0
- data/lib/assets/rule/compile/javascript/coffescript.rb +24 -0
- data/lib/assets/rule/compile/javascript.rb +12 -0
- data/lib/assets/rule/compile.rb +99 -0
- data/lib/assets/rule/concat.rb +97 -0
- data/lib/assets/rule/file.rb +67 -0
- data/lib/assets/rule/rename.rb +57 -0
- data/lib/assets/rule.rb +69 -0
- data/lib/assets.rb +37 -0
- data/spec/assets/application.coffee +1 -0
- data/spec/assets/application.sass +3 -0
- data/spec/assets/fonts.css +1 -0
- data/spec/assets/test.jpg +0 -0
- data/spec/integration/assets/spike_spec.rb +156 -0
- data/spec/spec_helper.rb +3 -0
- metadata +247 -0
@@ -0,0 +1,67 @@
|
|
1
|
+
module Assets
|
2
|
+
|
3
|
+
# Rule evaluator
|
4
|
+
class Evaluator
|
5
|
+
include Adamantium, Concord.new(:rule)
|
6
|
+
|
7
|
+
# Return asset
|
8
|
+
#
|
9
|
+
# @return [Asset]
|
10
|
+
#
|
11
|
+
# @api private
|
12
|
+
#
|
13
|
+
def asset
|
14
|
+
rule = self.rule
|
15
|
+
Asset.new(
|
16
|
+
:name => rule.name,
|
17
|
+
:mime => rule.mime,
|
18
|
+
:created_at => Time.now,
|
19
|
+
:body => body,
|
20
|
+
:size => size,
|
21
|
+
:sha1 => sha1
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Return sha1 hexdigest of body
|
26
|
+
#
|
27
|
+
# @return [String]
|
28
|
+
#
|
29
|
+
# @api private
|
30
|
+
#
|
31
|
+
def sha1
|
32
|
+
Digest::SHA1.hexdigest(body)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Return body
|
36
|
+
#
|
37
|
+
# @return [String]
|
38
|
+
#
|
39
|
+
# @api private
|
40
|
+
#
|
41
|
+
def body
|
42
|
+
rule.body
|
43
|
+
end
|
44
|
+
memoize :body
|
45
|
+
|
46
|
+
# Return size in bytes
|
47
|
+
#
|
48
|
+
# @return [Fixnum]
|
49
|
+
#
|
50
|
+
# @api private
|
51
|
+
#
|
52
|
+
def size
|
53
|
+
body.bytesize
|
54
|
+
end
|
55
|
+
|
56
|
+
# Return mime
|
57
|
+
#
|
58
|
+
# @return [Mime]
|
59
|
+
#
|
60
|
+
# @api private
|
61
|
+
#
|
62
|
+
def mime
|
63
|
+
rule.mime
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Assets
|
2
|
+
# Asset request handler
|
3
|
+
class Handler
|
4
|
+
include Adamantium, Concord.new(:environment, :prefix)
|
5
|
+
|
6
|
+
# Instantiate object
|
7
|
+
#
|
8
|
+
# @param [Environment] environment
|
9
|
+
# @param [String] prefix
|
10
|
+
#
|
11
|
+
# @return [undefined]
|
12
|
+
#
|
13
|
+
# @api private
|
14
|
+
#
|
15
|
+
def self.new(environment, prefix = '')
|
16
|
+
super(environment, Regexp.compile(%r(\A#{Regexp.escape(prefix)})))
|
17
|
+
end
|
18
|
+
|
19
|
+
# Call handler
|
20
|
+
#
|
21
|
+
# @param [Application] application
|
22
|
+
# @param [Request] request
|
23
|
+
#
|
24
|
+
# @return [Response]
|
25
|
+
#
|
26
|
+
# @api private
|
27
|
+
#
|
28
|
+
def call(_application, request)
|
29
|
+
name = request.path_info.gsub(prefix, '')
|
30
|
+
asset = environment.get(name)
|
31
|
+
if asset
|
32
|
+
Responder.run(request, asset)
|
33
|
+
else
|
34
|
+
Responder::NOT_FOUND
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
data/lib/assets/mime.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
module Assets
|
2
|
+
class Mime
|
3
|
+
include Adamantium, Concord.new(:extname, :content_type)
|
4
|
+
|
5
|
+
REGISTRY = {}
|
6
|
+
|
7
|
+
# Return extname
|
8
|
+
#
|
9
|
+
# @return [String]
|
10
|
+
#
|
11
|
+
# @api private
|
12
|
+
#
|
13
|
+
attr_reader :extname
|
14
|
+
|
15
|
+
# Return content type
|
16
|
+
#
|
17
|
+
# @return [String]
|
18
|
+
#
|
19
|
+
# @api private
|
20
|
+
#
|
21
|
+
attr_reader :content_type
|
22
|
+
|
23
|
+
# Return mime for extname
|
24
|
+
#
|
25
|
+
# @param [String] extname
|
26
|
+
#
|
27
|
+
# @return [Mime]
|
28
|
+
#
|
29
|
+
# @api private
|
30
|
+
#
|
31
|
+
def self.extname(extname)
|
32
|
+
REGISTRY.fetch(extname)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Return mime name
|
36
|
+
#
|
37
|
+
# @param [String] name
|
38
|
+
#
|
39
|
+
# @return [Mime]
|
40
|
+
#
|
41
|
+
# @api private
|
42
|
+
#
|
43
|
+
def self.from_name(name)
|
44
|
+
extname(::File.extname(name))
|
45
|
+
end
|
46
|
+
|
47
|
+
# Instantiate object
|
48
|
+
#
|
49
|
+
# @return [Mime]
|
50
|
+
#
|
51
|
+
# @api private
|
52
|
+
#
|
53
|
+
def self.new(*)
|
54
|
+
instance = super
|
55
|
+
REGISTRY[instance.extname]=instance
|
56
|
+
instance
|
57
|
+
end
|
58
|
+
|
59
|
+
private_class_method :new
|
60
|
+
|
61
|
+
JPG = new('.jpg', 'image/jpg' )
|
62
|
+
ICO = new('.ico', 'image/vnd.microsoft.icon' )
|
63
|
+
PNG = new('.png', 'image/png' )
|
64
|
+
GIF = new('.gif', 'image/gif' )
|
65
|
+
SVG = new('.svg', 'image/svg' )
|
66
|
+
PDF = new('.pdf', 'application/pdf' )
|
67
|
+
RUBY = new('.rb', 'application/ruby' )
|
68
|
+
TXT = new('.txt', 'text/plain; charset=UTF-8' )
|
69
|
+
CSS = new('.css', 'text/css; charset=UTF-8' )
|
70
|
+
JAVASCRIPT = new('.js', 'application/javascript; charset=UTF-8' )
|
71
|
+
COFFEESCRIPT = new('.coffee', 'application/coffeescript; charset=UTF-8' )
|
72
|
+
SASS = new('.sass', 'text/plain; charset=UTF-8' )
|
73
|
+
HTML = new('.html', 'text/html; charset=UTF-8' )
|
74
|
+
|
75
|
+
IMAGES = [
|
76
|
+
JPG, ICO, PNG, SVG, GIF
|
77
|
+
].freeze
|
78
|
+
|
79
|
+
REGISTRY.freeze
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
module Assets
|
2
|
+
class Package
|
3
|
+
include Concord::Public.new(:repository, :contents)
|
4
|
+
|
5
|
+
# Return rules for mime type
|
6
|
+
#
|
7
|
+
# @return [Enumerable<Rule>]
|
8
|
+
#
|
9
|
+
# @api private
|
10
|
+
#
|
11
|
+
def mime(mime)
|
12
|
+
contents.fetch(mime, [])
|
13
|
+
end
|
14
|
+
|
15
|
+
# Return image rules
|
16
|
+
#
|
17
|
+
# @return [Enumerable<Rule>]
|
18
|
+
#
|
19
|
+
# @api private
|
20
|
+
#
|
21
|
+
def images
|
22
|
+
Mime::IMAGES.each_with_object([]) do |mime, images|
|
23
|
+
images.concat(mime(mime))
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class Builder
|
28
|
+
|
29
|
+
# Run builder
|
30
|
+
#
|
31
|
+
# @param [String] directory
|
32
|
+
#
|
33
|
+
# @return [Package]
|
34
|
+
#
|
35
|
+
# @api private
|
36
|
+
#
|
37
|
+
def self.run(directory)
|
38
|
+
repository = Repository::Directory.new(directory)
|
39
|
+
builder = new(repository)
|
40
|
+
yield builder
|
41
|
+
Package.new(repository, builder.contents)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Initialize object
|
45
|
+
#
|
46
|
+
# @param [Repository] repository
|
47
|
+
#
|
48
|
+
# @return [undefined]
|
49
|
+
#
|
50
|
+
# @api private
|
51
|
+
#
|
52
|
+
def initialize(repository)
|
53
|
+
@repository = repository
|
54
|
+
@contents = Hash.new { |hash, key| hash[key] = [] }
|
55
|
+
end
|
56
|
+
|
57
|
+
# Return contents
|
58
|
+
#
|
59
|
+
# @return [Hash]
|
60
|
+
#
|
61
|
+
# @api private
|
62
|
+
#
|
63
|
+
attr_reader :contents
|
64
|
+
|
65
|
+
# Return repository
|
66
|
+
#
|
67
|
+
# @return [Repository]
|
68
|
+
#
|
69
|
+
# @api private
|
70
|
+
#
|
71
|
+
attr_reader :repository
|
72
|
+
|
73
|
+
# Add compile
|
74
|
+
#
|
75
|
+
# @param [String] name
|
76
|
+
#
|
77
|
+
# @return [self]
|
78
|
+
#
|
79
|
+
# @api private
|
80
|
+
#
|
81
|
+
def compile(name)
|
82
|
+
append(repository.compile(name))
|
83
|
+
end
|
84
|
+
|
85
|
+
# Add static file
|
86
|
+
#
|
87
|
+
# @param [String] name
|
88
|
+
#
|
89
|
+
# @return [self]
|
90
|
+
#
|
91
|
+
# @api private
|
92
|
+
#
|
93
|
+
def file(name)
|
94
|
+
append(repository.file(name))
|
95
|
+
end
|
96
|
+
|
97
|
+
# Perform glob
|
98
|
+
#
|
99
|
+
# @param [String] pattern
|
100
|
+
#
|
101
|
+
# @return [Enumerable<String>]
|
102
|
+
#
|
103
|
+
# @api private
|
104
|
+
#
|
105
|
+
def glob(pattern)
|
106
|
+
repository.glob(pattern)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Append rule
|
110
|
+
#
|
111
|
+
# @param [Rule] rule
|
112
|
+
#
|
113
|
+
# @return [self]
|
114
|
+
#
|
115
|
+
# @api private
|
116
|
+
#
|
117
|
+
def append(rule)
|
118
|
+
@contents[rule.mime] << rule
|
119
|
+
self
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Assets
|
2
|
+
|
3
|
+
# Abstract base class for asset repositories
|
4
|
+
class Repository
|
5
|
+
|
6
|
+
# Physical directory repository
|
7
|
+
class Directory
|
8
|
+
include Concord.new(:root)
|
9
|
+
|
10
|
+
# Build a file rule
|
11
|
+
#
|
12
|
+
# @param [#to_s] name
|
13
|
+
#
|
14
|
+
# @return [Rule::File]
|
15
|
+
#
|
16
|
+
# @api private
|
17
|
+
#
|
18
|
+
def file(name)
|
19
|
+
Rule::File.new(name.to_s, path(name))
|
20
|
+
end
|
21
|
+
|
22
|
+
# Return names matching pattern
|
23
|
+
#
|
24
|
+
# @param [String] pattern
|
25
|
+
#
|
26
|
+
# @return [Enumerable<String>]
|
27
|
+
#
|
28
|
+
# @api private
|
29
|
+
#
|
30
|
+
def glob(pattern)
|
31
|
+
root = self.root
|
32
|
+
Pathname.glob(root.join(pattern)).map do |match|
|
33
|
+
match.relative_path_from(root).to_s
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Return path for name
|
38
|
+
#
|
39
|
+
# @param [String] name
|
40
|
+
#
|
41
|
+
# @return [Pathname]
|
42
|
+
#
|
43
|
+
# @api private
|
44
|
+
#
|
45
|
+
def path(name)
|
46
|
+
root.join(name)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Build a compile rule
|
50
|
+
#
|
51
|
+
# @return [Rule::Compile::Sass]
|
52
|
+
#
|
53
|
+
# @api private
|
54
|
+
#
|
55
|
+
def compile(name)
|
56
|
+
Rule::Compile.build(file(name))
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
module Assets
|
2
|
+
# Abstract base class for asset responders
|
3
|
+
class Responder
|
4
|
+
include AbstractType, Concord.new(:asset)
|
5
|
+
|
6
|
+
HEADERS = IceNine.deep_freeze('Cache-Control' => 'max-age=120, must-revalidate')
|
7
|
+
NOT_FOUND = Response.build(
|
8
|
+
Response::Status::NOT_FOUND,
|
9
|
+
HEADERS.merge('Content-Type' => Assets::Mime::TXT.content_type),
|
10
|
+
'Not Found'
|
11
|
+
)
|
12
|
+
|
13
|
+
# Call responder
|
14
|
+
#
|
15
|
+
# @param [Object] asset
|
16
|
+
#
|
17
|
+
# @return [Response]
|
18
|
+
#
|
19
|
+
# @api private
|
20
|
+
#
|
21
|
+
def self.call(asset)
|
22
|
+
new(asset).response
|
23
|
+
end
|
24
|
+
|
25
|
+
# Run responder
|
26
|
+
#
|
27
|
+
# @param [Request] request
|
28
|
+
# @param [Asset] asset
|
29
|
+
#
|
30
|
+
# @return [Response] response
|
31
|
+
#
|
32
|
+
# @api private
|
33
|
+
#
|
34
|
+
def self.run(request, asset)
|
35
|
+
timestamp = request.if_modified_since
|
36
|
+
|
37
|
+
responder =
|
38
|
+
if timestamp && asset.fresh_at?(timestamp)
|
39
|
+
NotModified
|
40
|
+
else
|
41
|
+
New
|
42
|
+
end
|
43
|
+
|
44
|
+
responder.call(asset)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Return response
|
48
|
+
#
|
49
|
+
# @return [Response]
|
50
|
+
#
|
51
|
+
# @api private
|
52
|
+
#
|
53
|
+
def response
|
54
|
+
Response.new(status, headers, body)
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
# Return status code
|
60
|
+
#
|
61
|
+
# @return [Fixnum]
|
62
|
+
#
|
63
|
+
# @api private
|
64
|
+
#
|
65
|
+
def status
|
66
|
+
self.class::STATUS
|
67
|
+
end
|
68
|
+
|
69
|
+
# Return headers
|
70
|
+
#
|
71
|
+
# @return [Hash]
|
72
|
+
#
|
73
|
+
# @api private
|
74
|
+
#
|
75
|
+
def headers
|
76
|
+
HEADERS.merge('Last-Modified' => asset.created_at.httpdate)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Return content type header value
|
80
|
+
#
|
81
|
+
# @return [String]
|
82
|
+
#
|
83
|
+
# @api private
|
84
|
+
#
|
85
|
+
def content_type
|
86
|
+
asset.mime.content_type
|
87
|
+
end
|
88
|
+
|
89
|
+
# Not modified responder
|
90
|
+
class NotModified < self
|
91
|
+
STATUS = Response::Status::NOT_MODIFIED
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
# Return body
|
96
|
+
#
|
97
|
+
# @return [nil]
|
98
|
+
#
|
99
|
+
# @api private
|
100
|
+
#
|
101
|
+
def body; end
|
102
|
+
|
103
|
+
end
|
104
|
+
|
105
|
+
# New asset responder
|
106
|
+
class New < self
|
107
|
+
STATUS = Response::Status::OK
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
# Return headers
|
112
|
+
#
|
113
|
+
# @return [Hash]
|
114
|
+
#
|
115
|
+
# @api private
|
116
|
+
#
|
117
|
+
def headers
|
118
|
+
super.merge('Content-Type' => content_type)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Return body
|
122
|
+
#
|
123
|
+
# @return [String]
|
124
|
+
#
|
125
|
+
# @api private
|
126
|
+
#
|
127
|
+
def body
|
128
|
+
asset.body
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Assets
|
2
|
+
class Rule
|
3
|
+
class Compile
|
4
|
+
# Abstract base class for compilers that compile to css
|
5
|
+
class Css < self
|
6
|
+
|
7
|
+
MIME = Mime::CSS
|
8
|
+
|
9
|
+
# Compiler for sass
|
10
|
+
class Sass < self
|
11
|
+
handle(Mime::SASS)
|
12
|
+
|
13
|
+
# Return body
|
14
|
+
#
|
15
|
+
# @return [String]
|
16
|
+
#
|
17
|
+
# @api private
|
18
|
+
#
|
19
|
+
def body
|
20
|
+
::Sass.compile(input.body, :syntax => :sass)
|
21
|
+
end
|
22
|
+
|
23
|
+
end # Sass
|
24
|
+
|
25
|
+
end # Css
|
26
|
+
end # Compile
|
27
|
+
end # Rule
|
28
|
+
end # Assets
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Assets
|
2
|
+
class Rule
|
3
|
+
class Compile
|
4
|
+
class Javascript
|
5
|
+
|
6
|
+
# Compiler for sass
|
7
|
+
class Coffescript < self
|
8
|
+
handle(Mime::COFFEESCRIPT)
|
9
|
+
|
10
|
+
# Return body
|
11
|
+
#
|
12
|
+
# @return [String]
|
13
|
+
#
|
14
|
+
# @api private
|
15
|
+
#
|
16
|
+
def body
|
17
|
+
::CoffeeScript.compile(input.body)
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module Assets
|
2
|
+
class Rule
|
3
|
+
class Compile < self
|
4
|
+
include Concord.new(:input)
|
5
|
+
|
6
|
+
# Return name
|
7
|
+
#
|
8
|
+
# @return [String]
|
9
|
+
#
|
10
|
+
# @api private
|
11
|
+
#
|
12
|
+
def name
|
13
|
+
input.name
|
14
|
+
end
|
15
|
+
|
16
|
+
# Return updated at
|
17
|
+
#
|
18
|
+
# @return [Time]
|
19
|
+
#
|
20
|
+
# @api private
|
21
|
+
#
|
22
|
+
def updated_at
|
23
|
+
input.updated_at
|
24
|
+
end
|
25
|
+
|
26
|
+
# Return target name
|
27
|
+
#
|
28
|
+
# @return [String]
|
29
|
+
#
|
30
|
+
# @api private
|
31
|
+
#
|
32
|
+
def target_name
|
33
|
+
regexp = %r(#{Regexp.escape(input.extname)}\z)
|
34
|
+
name.gsub(regexp, mime.extname)
|
35
|
+
end
|
36
|
+
memoize :target_name
|
37
|
+
|
38
|
+
# Return registry
|
39
|
+
#
|
40
|
+
# @return [Hash]
|
41
|
+
#
|
42
|
+
# @api private
|
43
|
+
#
|
44
|
+
def self.registry
|
45
|
+
@registry ||= {}
|
46
|
+
end
|
47
|
+
|
48
|
+
# protected_class_method :registry
|
49
|
+
singleton_class.send(:protected, :registry)
|
50
|
+
|
51
|
+
# Register handler
|
52
|
+
#
|
53
|
+
# @param [Mime] mime
|
54
|
+
#
|
55
|
+
# @return [undefined]
|
56
|
+
#
|
57
|
+
# @api private
|
58
|
+
#
|
59
|
+
def self.handle(mime)
|
60
|
+
Compile.registry[mime]=self
|
61
|
+
end
|
62
|
+
private_class_method :handle
|
63
|
+
|
64
|
+
# Build compiler rule
|
65
|
+
#
|
66
|
+
# @param [Rule::File] rule
|
67
|
+
#
|
68
|
+
# @return [Rule::Compile]
|
69
|
+
#
|
70
|
+
# @api private
|
71
|
+
#
|
72
|
+
def self.build(rule)
|
73
|
+
builder = Compile.registry.fetch(rule.mime)
|
74
|
+
compiler = builder.new(rule)
|
75
|
+
Rename.new(compiler.target_name, compiler)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Return mime type
|
79
|
+
#
|
80
|
+
# @return [Mime]
|
81
|
+
#
|
82
|
+
# @api private
|
83
|
+
#
|
84
|
+
def mime
|
85
|
+
self.class.mime
|
86
|
+
end
|
87
|
+
|
88
|
+
# Return mime type
|
89
|
+
#
|
90
|
+
# @return [Mime]
|
91
|
+
#
|
92
|
+
# @api private
|
93
|
+
#
|
94
|
+
def self.mime
|
95
|
+
self::MIME
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|