jsus 0.3.1 → 0.3.2

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.
@@ -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