mbj-assets 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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,12 @@
1
+ module Assets
2
+ class Rule
3
+ class Compile
4
+ # Base class for compilers that compile to javascript
5
+ class Javascript < self
6
+
7
+ MIME = Mime::JAVASCRIPT
8
+
9
+ end
10
+ end
11
+ end
12
+ 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