jsus 0.3.1 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,75 +1,114 @@
1
1
  require 'rack/utils'
2
2
  module Jsus
3
3
  #
4
- # Rack middleware
4
+ # Jsus rack middleware.
5
5
  #
6
- # Just "use Jsus::Middleware" in your rack application and all the requests
7
- # to /javascripts/jsus/* are redirected to the middleware.
6
+ # Usage
7
+ # -----
8
8
  #
9
- # Configuration:
9
+ # `use Jsus::Middleware` in your rack application and all the requests
10
+ # to /javascripts/jsus/* will be redirected to the middleware.
10
11
  #
11
- # Use Jsus::Middleware.settings= method to change some settings, such as:
12
- # :packages_dir - path to where you store your packages
13
- # :cache - enable simple file caching
14
- # :cache_path - directory for file caching
15
- # :prefix - change /jsus/ to something else or remove it altogether (set to nil)
16
- # :cache_pool - cache js pool between requests. Can save you some time
17
- # between requests but annoys a lot during development.
12
+ # Example requests
13
+ # ----------------
18
14
  #
15
+ # <dt>`GET /javascripts/jsus/require/Mootools.Core+Mootools.More`</dt>
16
+ # <dd>merges packages named Mootools.Core and Mootools.More with all the
17
+ # dependencies and outputs the result.</dd>
19
18
  #
20
- # Examples:
19
+ # <dt>`GET /javascripts/jsus/require/Mootools.More~Mootools.Core`</dt>
20
+ # <dd>returns package Mootools.More with all the dependencies MINUS any of
21
+ # Mootools.Core dependencies.</dd>
21
22
  #
22
- # GET /javascripts/jsus/require/Mootools.Core+Mootools.More
23
- # merges packages named Mootools.Core and Mootools.More with all the
24
- # dependencies and outputs the result.
23
+ # <dt>`GET /javascripts/jsus/require/Mootools.Core:Class+Mootools.More:Fx`</dt>
24
+ # <dd>same thing but for source files providing Mootools.Core/Class and
25
+ # Mootools.More/Fx</dd>
25
26
  #
26
- # GET /javascripts/jsus/require/Mootools.More~Mootools.Core
27
- # returns package Mootools.More with all the dependencies MINUS any of
28
- # Mootools.Core dependencies.
27
+ # <dt>`GET /javascripts/jsus/include/Mootools.Core`</dt>
28
+ # <dd>generates js file with remote javascript fetching via ajax</dd>
29
29
  #
30
- # GET /javascripts/jsus/require/Mootools.Core:Class+Mootools.More:Fx
31
- # same thing but for source files providing Mootools.Core/Class and
32
- # Mootools.More/Fx
33
- #
34
- #
35
- # Also see sinatra example https://github.com/jsus/jsus-sinatra-app
30
+ # @see .settings=
31
+ # @see https://github.com/jsus/jsus-sinatra-app Sinatra example (Github)
32
+ # @see https://github.com/jsus/jsus-rails-app Rails example (Github)
36
33
  #
37
34
  class Middleware
38
35
  include Rack
39
36
  class <<self
37
+ # Default settings for Middleware
40
38
  DEFAULT_SETTINGS = {
41
39
  :packages_dir => ".",
42
40
  :cache => false,
43
41
  :cache_path => nil,
44
42
  :prefix => "jsus",
45
- :cache_pool => true
43
+ :cache_pool => true,
44
+ :includes_root => "."
46
45
  }.freeze
47
46
 
47
+ # @return [Hash] Middleware current settings
48
+ # @api public
48
49
  def settings
49
- @settings ||= DEFAULT_SETTINGS.dup
50
+ @@settings ||= DEFAULT_SETTINGS.dup
50
51
  end # settings
51
52
 
53
+ # *Merges* given new settings into current settings.
54
+ #
55
+ # @param [Hash] new_settings
56
+ # @option new_settings [String, Array] :packages_dir directory (or array
57
+ # of directories) containing source files.
58
+ # @option new_settings [Boolean] :cache enable file caching (every request
59
+ # is written into corresponding file). Note, that it's write-only cache,
60
+ # you will have to configure your webserver to serve these files.
61
+ # @option new_settings [String] :cache_path path to cache directory
62
+ # @option new_settings [String, nil] :prefix path prefix to use for
63
+ # request. You can change default "jsus" to anything else or disable it
64
+ # altogether.
65
+ # @option new_settings [Boolean] :cache_pool whether to cache pool between
66
+ # requests. Cached pool means that updates to your source files will not
67
+ # be visible until you restart webserver.
68
+ # @option new_settings [String] :includes_root when generating "includes"
69
+ # lists, this is the point in filesystem used as relative root.
70
+ # @api public
52
71
  def settings=(new_settings)
53
72
  settings.merge!(new_settings)
54
73
  end # settings=
55
74
 
75
+ # Generates and caches a pool of source files and packages.
76
+ #
77
+ # @return [Jsus::Pool]
78
+ # @api public
56
79
  def pool
57
- @pool ||= Jsus::Pool.new(settings[:packages_dir])
80
+ @@pool ||= Jsus::Pool.new(settings[:packages_dir])
58
81
  end # pool
59
82
 
83
+ # @return [Boolean] whether caching is enabled
84
+ # @api public
60
85
  def cache?
61
86
  settings[:cache]
62
87
  end # cache?
63
88
 
89
+ # @return [Jsus::Util::FileCache] file cache to store results of requests.
90
+ # @api public
64
91
  def cache
65
- @cache ||= cache? ? Util::FileCache.new(settings[:cache_path]) : nil
92
+ @@cache ||= cache? ? Util::FileCache.new(settings[:cache_path]) : nil
66
93
  end # cache
67
94
  end # class <<self
68
95
 
96
+ # Default rack initialization routine
97
+ # @param [#call] rack chain caller
98
+ # @api public
69
99
  def initialize(app)
70
100
  @app = app
71
101
  end # initialize
72
102
 
103
+ # Since rack apps are used as singletons and we store some state during
104
+ # request handling, we need to separate state between different calls.
105
+ #
106
+ # Jsus::Middleware#call method dups current rack app and executes
107
+ # Jsus::Middleware#_call on it.
108
+ #
109
+ # @param [Hash] rack env
110
+ # @return [#each] rack response
111
+ # @api semipublic
73
112
  def _call(env)
74
113
  path = Utils.unescape(env["PATH_INFO"])
75
114
  return @app.call(env) unless handled_by_jsus?(path)
@@ -77,37 +116,77 @@ module Jsus
77
116
  components = path.split("/")
78
117
  return @app.call(env) unless components.size >= 2
79
118
  if components[0] == "require"
80
- generate(components[1])
119
+ generate_requires(components[1])
120
+ elsif components[0] == "include"
121
+ generate_includes(components[1])
81
122
  else
82
123
  not_found!
83
124
  end
84
125
  end # _call
85
126
 
127
+ # Rack calling point
128
+ #
129
+ # @param [Hash] rack env
130
+ # @return [#each] rack response
131
+ # @api public
86
132
  def call(env)
87
133
  dup._call(env)
88
134
  end # call
89
135
 
90
136
  protected
91
137
 
92
- def generate(path_string)
93
- path_args = parse_path_string(path_string.sub(/.js$/, ""))
94
- files = []
95
- path_args[:include].each {|tag| files += get_associated_files(tag).to_a }
96
- path_args[:exclude].each {|tag| files -= get_associated_files(tag).to_a }
138
+ # Generates response for /require/ requests.
139
+ #
140
+ # @param [String] path component to required sources
141
+ # @return [#each] rack response
142
+ # @api semipublic
143
+ def generate_requires(path_string)
144
+ files = path_string_to_files(path_string)
97
145
  if !files.empty?
98
146
  response = Container.new(*files).map {|f| f.content }.join("\n")
99
- cache.write(path_string, response) if cache?
147
+ cache.write(escape_path_for_cache_key(path_string), response) if cache?
100
148
  respond_with(response)
101
149
  else
102
150
  not_found!
103
151
  end
104
- end # generate
152
+ end # generate_requires
153
+
154
+ # Generates response for /include/ requests.
155
+ #
156
+ # @param [String] path component to included sources
157
+ # @return [#each] rack response
158
+ # @api semipublic
159
+ def generate_includes(path_string)
160
+ files = path_string_to_files(path_string)
161
+ if !files.empty?
162
+ paths = Container.new(*files).required_files(self.class.settings[:includes_root])
163
+ respond_with(Jsus::Util::CodeGenerator.generate_includes(paths))
164
+ else
165
+ not_found!
166
+ end
167
+ end # generate_includes
105
168
 
106
- # Notice: + is a space after url decoding
107
- # input:
108
- # "Package:A~Package:C Package:B~Other:D"
109
- # output:
110
- # {:include => ["Package/A", "Package/B"], :exclude => ["Package/C", "Other/D"]}
169
+ # Returns list of exlcuded and included source files for given path strings.
170
+ #
171
+ # @param [String] string with + and ~
172
+ # @return [Hash] hash with source files to include and to exclude
173
+ # @api semipublic
174
+ def path_string_to_files(path_string)
175
+ path_args = parse_path_string(path_string.sub(/.js$/, ""))
176
+ files = []
177
+ path_args[:include].each {|tag| files += get_associated_files(tag).to_a }
178
+ path_args[:exclude].each {|tag| files -= get_associated_files(tag).to_a }
179
+ files
180
+ end # path_string_to_files
181
+
182
+ # Parses human-readable string with + and ~ operations into a more usable hash.
183
+ # @note + is a space after url decoding
184
+ #
185
+ # @example
186
+ # parse_path_string("Package:A~Package:C Package:B~Other:D")
187
+ # => {:include => ["Package/A", "Package/B"],
188
+ # :exclude => ["Package/C", "Other/D"]}
189
+ # @api semipublic
111
190
  def parse_path_string(path_string)
112
191
  path_string = " " + path_string unless path_string[0,1] =~ /\+\-/
113
192
  included = []
@@ -123,6 +202,11 @@ module Jsus
123
202
  {:include => included, :exclude => excluded}
124
203
  end # parse_path_string
125
204
 
205
+ # Returns a list of associated files for given source file or source package.
206
+ # @param [String] canonical source file or source package name or wildcard
207
+ # e.g. "Mootools.Core", "Mootools.Core/*", "Mootools.Core/Class", "**/*"
208
+ # @return [Array] list of source files for given input
209
+ # @api semipublic
126
210
  def get_associated_files(source_file_or_package)
127
211
  if package = pool.packages.detect {|pkg| pkg.name == source_file_or_package}
128
212
  package.include_dependencies!
@@ -132,7 +216,7 @@ module Jsus
132
216
  else
133
217
  # Try using arg as mask
134
218
  mask = source_file_or_package.to_s
135
- if !(mask =~ /^\s*$/) && !(source_files = pool.provides_tree.glob(mask)).empty?
219
+ if !(mask =~ /^\s*$/) && !(source_files = pool.provides_tree.glob(mask).compact).empty?
136
220
  source_files.map {|source| get_associated_files(source) }.flatten
137
221
  else
138
222
  # No dice
@@ -141,27 +225,44 @@ module Jsus
141
225
  end
142
226
  end # get_associated_files
143
227
 
228
+ # Rack response of not found
229
+ # @return [#each] 404 response
230
+ # @api semipublic
144
231
  def not_found!
145
232
  [404, {"Content-Type" => "text/plain"}, ["Jsus doesn't know anything about this entity"]]
146
233
  end # not_found!
147
234
 
235
+ # Respond with given text
236
+ # @param [String] text to respond with
237
+ # @return [#each] 200 response
238
+ # @api semipublic
148
239
  def respond_with(text)
149
240
  [200, {"Content-Type" => "text/javascript"}, [text]]
150
241
  end # respond_with
151
242
 
152
-
243
+ # Check whether given path is handled by jsus middleware.
244
+ #
245
+ # @param [String] path
246
+ # @return [Boolean]
247
+ # @api semipublic
153
248
  def handled_by_jsus?(path)
154
249
  path =~ path_prefix_regex
155
250
  end # handled_by_jsus?
156
251
 
252
+ # @return [String] Jsus request path prefix
253
+ # @api semipublic
157
254
  def path_prefix
158
255
  @path_prefix ||= self.class.settings[:prefix] ? "/javascripts/#{self.class.settings[:prefix]}/" : "/javascripts/"
159
256
  end # path_prefix
160
257
 
258
+ # @return [Regexp] Jsus request path regexp
259
+ # @api semipublic
161
260
  def path_prefix_regex
162
261
  @path_prefix_regex ||= %r{^#{path_prefix}}
163
262
  end # path_prefix_regex
164
263
 
264
+ # @return [Jsus::Pool] Jsus session pool
265
+ # @api semipublic
165
266
  def pool
166
267
  if cache_pool?
167
268
  self.class.pool
@@ -170,16 +271,32 @@ module Jsus
170
271
  end
171
272
  end # pool
172
273
 
274
+ # @return [Boolean] whether request is going to be cached
275
+ # @api semipublic
173
276
  def cache?
174
277
  self.class.cache?
175
278
  end # cache?
176
279
 
280
+ # @return [Jsus::Util::FileCache] file cache to store response
281
+ # @api semipublic
177
282
  def cache
178
283
  self.class.cache
179
284
  end # cache
180
285
 
286
+ # @return [Boolean] whether pool is shared between requests
287
+ # @api semipublic
181
288
  def cache_pool?
182
289
  self.class.settings[:cache_pool]
183
290
  end # cache_pool?
291
+
292
+ # You might or might not need to do some last minute conversions for your cache
293
+ # key. Default behaviour is merely a nginx hack, you may have to use your own
294
+ # function for your web-server.
295
+ # @param [String] request path minus the prefix
296
+ # @return [String] normalized cache key for given request path
297
+ # @api semipublic
298
+ def escape_path_for_cache_key(path)
299
+ path.gsub(" ", "+")
300
+ end # escape_path_for_cache_key
184
301
  end # class Middleware
185
302
  end # module Jsus
data/lib/jsus/package.rb CHANGED
@@ -4,25 +4,28 @@ module Jsus
4
4
  # a javascript package.
5
5
  #
6
6
  class Package
7
- attr_accessor :directory # directory which this package resides in (full path)
8
- attr_accessor :pool # an instance of Pool
7
+ # directory which this package resides in (full path)
8
+ attr_accessor :directory
9
+ # an instance of Jsus::Pool
10
+ attr_accessor :pool
11
+
9
12
  # Constructors
10
13
 
11
14
  #
12
15
  # Creates a package from given directory.
13
16
  #
14
- # Accepts options:
15
- # * +:pool:+ -- which pool the package should belong to.
16
- #
17
- # Raises an error when the given directory doesn't contain a package.yml or package.json
17
+ # @param [String] path to directory containing a package
18
+ # @param [Hash] options
19
+ # @option options [Jsus::Pool] :pool which pool the package should belong to.
20
+ # @raise an error when the given directory doesn't contain a package.yml or package.json
18
21
  # file with meta info.
19
- #
22
+ # @api public
20
23
  def initialize(directory, options = {})
21
24
  self.directory = File.expand_path(directory)
22
25
  if File.exists?(File.join(directory, 'package.yml'))
23
26
  self.header = YAML.load_file(File.join(directory, 'package.yml'))
24
27
  elsif File.exists?(File.join(directory, 'package.json'))
25
- self.header = JSON.load(IO.read(File.join(directory, 'package.json')))
28
+ self.header = JSON.load(File.open(File.join(directory, 'package.json'), 'r:utf-8') {|f| f.read })
26
29
  else
27
30
  raise "Directory #{directory} does not contain a valid package.yml / package.json file!"
28
31
  end
@@ -49,44 +52,52 @@ module Jsus
49
52
 
50
53
  # Public API
51
54
 
52
- # Returns a package.yml header.
55
+ # @return [Hash] parsed package header.
56
+ # @api public
53
57
  def header
54
58
  @header ||= {}
55
59
  end
56
60
 
57
- # Returns a package name.
61
+ # @return [String] a package name.
62
+ # @api public
58
63
  def name
59
64
  header["name"] ||= ""
60
65
  end
61
66
 
62
- # Returns a package description.
67
+ # @return [String] a package description.
68
+ # @api public
63
69
  def description
64
70
  header["description"] ||= ""
65
71
  end
66
72
 
67
- # Returns a filename for compiled package.
73
+ # @return [String] a filename for compiled package.
74
+ # @api public
68
75
  def filename
69
76
  header["filename"] ||= Jsus::Util::Inflection.snake_case(name) + ".js"
70
77
  end
71
78
 
72
- # Returns a list of sources filenames.
79
+ # @return [Array] a list of sources filenames.
80
+ # @api public
73
81
  def files
74
82
  header["files"] = header["files"] || header["sources"] || []
75
83
  end
76
84
  alias_method :sources, :files
77
85
 
78
- # Returns an array of provided tags including those provided by linked external dependencies.
86
+ # @return [Array] an array of provided tags including those provided by linked external dependencies.
87
+ # @api public
79
88
  def provides
80
89
  source_files.map {|s| s.provides }.flatten | linked_external_dependencies.map {|d| d.provides }.flatten
81
90
  end
82
91
 
83
- # Returns an array of provided tags names including those provided by linked external dependencies.
92
+ # @return [Array] an array of provided tags names including those provided by linked external dependencies.
93
+ # @api public
84
94
  def provides_names
85
95
  source_files.map {|s| s.provides_names(:short => true) }.flatten |
86
96
  linked_external_dependencies.map {|d| d.provides_names }.flatten
87
97
  end
88
98
 
89
- # Returns an array of unresolved dependencies' tags for the package.
99
+ # @return [Array] an array of unresolved dependencies' tags for the package.
100
+ # @api public
90
101
  def dependencies
91
102
  result = source_files.map {|source| source.dependencies }.flatten
92
103
  result |= linked_external_dependencies.map {|d| d.dependencies}.flatten
@@ -94,33 +105,44 @@ module Jsus
94
105
  result
95
106
  end
96
107
 
97
- # Returns an array of unresolved dependencies' names.
108
+ # @return [Array] an array of unresolved dependencies' names.
109
+ # @api public
98
110
  def dependencies_names
99
111
  dependencies.map {|d| d.name(:short => true) }
100
112
  end
101
113
 
102
- # Returns an array of external dependencies' tags (including resolved ones).
114
+ # @return [Array] an array of external dependencies' tags (including resolved ones).
115
+ # @api public
103
116
  def external_dependencies
104
117
  source_files.map {|s| s.external_dependencies }.flatten
105
118
  end
106
119
 
107
- # Returns an array of external dependencies' names (including resolved ones).
120
+ # @return [Array] an array of external dependencies' names (including resolved ones).
121
+ # @api public
108
122
  def external_dependencies_names
109
123
  external_dependencies.map {|d| d.name }
110
124
  end
111
125
 
112
- # Returns source files with external dependencies in correct order.
126
+ # @return [Jsus::Container] source files with external dependencies in correct order.
127
+ # @api public
113
128
  def linked_external_dependencies
114
129
  @linked_external_dependencies ||= Container.new
115
130
  end
116
131
 
117
132
  # Compiles source files and linked external source files into a given category.
133
+ # @param [String, nil] directory to output the result into
134
+ # @return [String] content of merged source files
135
+ # @api public
118
136
  def compile(directory = ".")
119
137
  fn = directory ? File.join(directory, filename) : nil
120
138
  Packager.new(*(source_files.to_a + linked_external_dependencies.to_a)).pack(fn)
121
139
  end
122
140
 
123
141
  # Generates tree structure for files in package into a json file.
142
+ # @param [String] directory to output the result
143
+ # @param [String] resulting filename
144
+ # @return [Hash] hash with tree structure
145
+ # @api public
124
146
  def generate_tree(directory = ".", filename = "tree.json")
125
147
  FileUtils.mkdir_p(directory)
126
148
  result = ActiveSupport::OrderedHash.new
@@ -140,6 +162,10 @@ module Jsus
140
162
  end
141
163
 
142
164
  # Generates info about resulting compiled package into a json file.
165
+ # @param [String] directory to output the result
166
+ # @param [String] resulting filename
167
+ # @return [Hash] hash with scripts info
168
+ # @api public
143
169
  def generate_scripts_info(directory = ".", filename = "scripts.json")
144
170
  FileUtils.mkdir_p directory
145
171
  File.open(File.join(directory, filename), "w") { |resulting_file| resulting_file << JSON.pretty_generate(self.to_hash) }
@@ -147,6 +173,7 @@ module Jsus
147
173
  end
148
174
 
149
175
  # Looks up all the external dependencies in the pool.
176
+ # @api semipublic
150
177
  def include_dependencies!
151
178
  source_files.each do |source|
152
179
  if pool
@@ -157,6 +184,7 @@ module Jsus
157
184
  end
158
185
 
159
186
  # Executes #include_extensions for all the source files.
187
+ # @api semipublic
160
188
  def include_extensions!
161
189
  source_files.each do |source|
162
190
  source.include_extensions!
@@ -164,11 +192,15 @@ module Jsus
164
192
  end
165
193
 
166
194
  # Lists the required files for the package.
195
+ # @return [Array] ordered list of full paths to required files.
196
+ # @api public
167
197
  def required_files
168
198
  source_files.map {|s| s.required_files }.flatten
169
199
  end
170
200
 
171
- def to_hash # :nodoc:
201
+ # Hash representation of the package.
202
+ # @api public
203
+ def to_hash
172
204
  {
173
205
  name => {
174
206
  :desc => description,
@@ -178,26 +210,34 @@ module Jsus
178
210
  }
179
211
  end
180
212
 
213
+
181
214
  # Container with source files
215
+ # @return [Jsus::Container]
216
+ # @api semipublic
182
217
  def source_files
183
218
  @source_files ||= Container.new
184
219
  end
185
220
 
186
221
  # Container with extensions (they aren't compiled or included into #reqired_files list)
222
+ # @return [Jsus::Container]
223
+ # @api semipublic
187
224
  def extensions
188
225
  @extensions ||= Container.new
189
226
  end
190
227
 
191
228
  # Private API
192
229
 
193
- def header=(new_header) # :nodoc:
230
+
231
+ # @param [Hash] parsed header
232
+ # @api private
233
+ def header=(new_header)
194
234
  @header = new_header
195
235
  end
196
236
 
197
- def linked_external_dependencies=(new_value) # :nodoc:
237
+ # @param [Enumerable] external dependencies
238
+ # @api private
239
+ def linked_external_dependencies=(new_value)
198
240
  @linked_external_dependencies = new_value
199
241
  end
200
-
201
- protected
202
242
  end
203
243
  end