rascut 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,4 @@
1
+ == 0.1.0 / 2007-09-02
2
+
3
+ * 1st Release.
4
+
data/Manifest.txt ADDED
@@ -0,0 +1,18 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ bin/rascut
6
+ lib/rascut.rb
7
+ lib/rascut/command.rb
8
+ lib/rascut/config.rb
9
+ lib/rascut/fcsh_wrapper.rb
10
+ lib/rascut/file_observer.rb
11
+ lib/rascut/httpd.rb
12
+ lib/rascut/logger.rb
13
+ lib/rascut/plugin/base.rb
14
+ lib/rascut/plugin/write_fcsh_error_output.rb
15
+ vendor/ruby/expect.rb
16
+ vendor/js/swfobject.js
17
+ test/test_rascut.rb
18
+ test/test_file_observer.rb
data/README.txt ADDED
@@ -0,0 +1,59 @@
1
+ rascut
2
+ by Yuichi Tateno
3
+
4
+ == DESCRIPTION:
5
+
6
+ Rascut is Ruby ActionSCript UTility :D
7
+
8
+ == FEATURES/PROBLEMS:
9
+
10
+
11
+ == SYNOPSIS:
12
+
13
+ * 1. flex2sdk and fcwrap setting.
14
+ * 2. $ rascut -s MyScript.as
15
+ * 3. Browser access http://localhost:3001/
16
+
17
+ == REQUIREMENTS:
18
+
19
+ * hoe
20
+ * rack
21
+ * mongrel
22
+ * rake
23
+
24
+ == INSTALL:
25
+
26
+ * gem install rascut
27
+
28
+ == OTHER:
29
+ * vendor/js/swfobject.js is SWFObject by http://blog.deconcept.com/swfobject/
30
+
31
+ == LICENSE:
32
+
33
+ (The MIT License)
34
+
35
+ Copyright (c) 2007 Yuichi Tateno
36
+
37
+ Permission is hereby granted, free of charge, to any person obtaining
38
+ a copy of this software and associated documentation files (the
39
+ 'Software'), to deal in the Software without restriction, including
40
+ without limitation the rights to use, copy, modify, merge, publish,
41
+ distribute, sublicense, and/or sell copies of the Software, and to
42
+ permit persons to whom the Software is furnished to do so, subject to
43
+ the following conditions:
44
+
45
+ The above copyright notice and this permission notice shall be
46
+ included in all copies or substantial portions of the Software.
47
+
48
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
49
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
50
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
51
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
52
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
53
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
54
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
55
+
56
+ == SPECIAL THANKS
57
+ Thank you feedback!
58
+
59
+ id:os0x, yugui, id:wanpark, saqoosha, id:ameema
data/Rakefile ADDED
@@ -0,0 +1,23 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+ $LOAD_PATH << './lib'
6
+ require './lib/rascut.rb'
7
+
8
+ Hoe.new('rascut', Rascut::VERSION) do |p|
9
+ p.rubyforge_name = 'hotchpotch'
10
+ p.author = 'yuichi tateno'
11
+ p.email = 'hotchpotch@nononospam@gmail.com'
12
+ p.summary = 'Ruby ActionSCript UTility'
13
+ p.description = p.paragraphs_of('README.txt', 2..5).join("\n\n")
14
+ p.url = p.paragraphs_of('README.txt', 0).first.split(/\n/)[1..-1]
15
+ p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
16
+
17
+ p.extra_deps << ['mongrel']
18
+ p.extra_deps << ['rake']
19
+ p.extra_deps << ['hoe']
20
+ p.extra_deps << ['rack']
21
+ end
22
+
23
+ # vim: syntax=Ruby
data/bin/rascut ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin
4
+ require 'rascut'
5
+ rescue LoadError
6
+ require 'rubygems'
7
+ require 'rascut'
8
+ end
9
+
10
+ Rascut::Command.new.run ARGV
@@ -0,0 +1,148 @@
1
+ require 'optparse'
2
+ require 'rascut/fcsh_wrapper'
3
+ require 'rascut/logger'
4
+ require 'rascut/config'
5
+ require 'pathname'
6
+ require 'yaml'
7
+
8
+ module Rascut
9
+ class Command
10
+ def initialize
11
+ @logger = Logger.new(STDOUT)
12
+ end
13
+ attr_accessor :logger
14
+
15
+ def run(argv)
16
+ @config = Config.new
17
+
18
+ if ENV['HOME']
19
+ home = Pathname.new ENV['HOME']
20
+ end
21
+
22
+ home.join('.rascut').mkpath if home
23
+
24
+ if home && home.join('.rascutrc').readable?
25
+ @config.merge_config home.join('.rascutrc')
26
+ end
27
+
28
+ if File.readable?('.rascut')
29
+ @config.merge_config('.rascut')
30
+ end
31
+
32
+ @config.parse_argv!(argv)
33
+
34
+ unless @target_script = argv.first
35
+ warn 'Target script is not found.'
36
+ Kernel::exit 1
37
+ end
38
+
39
+ @root = Pathname.new(@target_script).dirname.realpath
40
+ @wrapper = FcshWrapper.new(@target_script, @config)
41
+
42
+ start_server if @config[:server]
43
+ setting_signals
44
+ @wrapper.hooks[:compile_success] << method(:compile_success_proc)
45
+
46
+
47
+ if @config[:file_observing]
48
+ @file_observer = FileObserver.new(@config[:observe_files],
49
+ :interval => @config[:interval],
50
+ :ext => @config[:ext],
51
+ :logger => @config[:logger],
52
+ :update_handler => method(:file_update_handler))
53
+ @file_observer.run
54
+ end
55
+
56
+ read_log_loop if @config[:flashlog]
57
+
58
+ init_plugins
59
+
60
+ @wrapper.compile
61
+ #readline_loop
62
+ Thread.stop
63
+ end
64
+ attr_reader :config, :root, :target_script, :wrapper, :file_observer
65
+
66
+ def init_plugins
67
+ @config[:plugin].each do |name|
68
+ klass_name = name.gsub(/(^|_)(.)/) { $2.upcase }
69
+ logger.info "Load Plugin: #{klass_name}"
70
+ require "rascut/plugin/#{name}"
71
+ ::Rascut::Plugin.const_get(klass_name).new(self).run
72
+ end if @config[:plugin]
73
+ end
74
+
75
+ def file_update_handler
76
+ @wrapper.compile
77
+ end
78
+
79
+ def read_log_loop
80
+ log = Pathname.new(@config.params[:flashlog])
81
+ return unless (log && log.file?)
82
+
83
+ Thread.new(log) do |log|
84
+ flashlog_timestamp ||= log.mtime
85
+
86
+ log.open('r') do |f|
87
+ f.read
88
+ loop do
89
+ if log.mtime > flashlog_timestamp
90
+ f.rewind
91
+ text = f.read
92
+ if text.length == 0
93
+ f.rewind
94
+ text = f.read
95
+ end
96
+ logger.info("FLASHLOG\n" + text) unless text.strip.empty?
97
+ flashlog_timestamp = log.mtime
98
+ end
99
+ sleep 1
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ def compile_success_proc
106
+ if @httpd
107
+ @httpd.reload!
108
+ end
109
+ end
110
+
111
+ def start_server
112
+ require 'rascut/httpd'
113
+ @httpd = Httpd.new(self)
114
+ @httpd.start
115
+ end
116
+
117
+ def setting_signals
118
+ methods(true).each do |mname|
119
+ if m = mname.match(/^sig_(.+)$/)
120
+ begin
121
+ Signal.trap(m[1].upcase) { method(mname).call }
122
+ rescue ArgumentError
123
+ end
124
+ end
125
+ end
126
+ end
127
+
128
+ def sig_int
129
+ logger.debug 'SIG_INT'
130
+ self.exit()
131
+ end
132
+
133
+ def sig_usr2
134
+ logger.debug 'SIG_USR2'
135
+ @wrapper.compile
136
+ end
137
+
138
+ def exit
139
+ logger.info 'exiting...'
140
+ begin
141
+ @wrapper.close
142
+ rescue Exception => e
143
+ logger.error e.inspect
144
+ end
145
+ Kernel::exit 1
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,82 @@
1
+ require 'optparse'
2
+ require 'rascut/logger'
3
+ require 'logger'
4
+
5
+ module Rascut
6
+ class Config
7
+ DEFAULT_CONFIG = {
8
+ :interval => 1,
9
+ :compile_config => nil,
10
+ :file_observing => true,
11
+ :fcsh_cmd => 'fcsh',
12
+ :ext => ['as', 'css', 'mxml'],
13
+ :logger => Rascut::Logger.new(STDOUT),
14
+ }
15
+
16
+ def initialize
17
+ @params = DEFAULT_CONFIG.dup
18
+ @params[:logger].level = ::Logger::INFO
19
+ end
20
+ attr_accessor :params
21
+
22
+ def [](name)
23
+ @params[name]
24
+ end
25
+
26
+ def logger
27
+ @params[:logger]
28
+ end
29
+
30
+ def parse_argv!(argv)
31
+ op = OptionParser.new
32
+ op.banner = 'Usage: $ rascut HelloWrold.as'
33
+
34
+ #op.on('-a', 'Apollo compile mode') do |v|
35
+ # @params[:apollo] = true
36
+ # @params[:compile_config] = '+configname=apollo'
37
+ #end
38
+
39
+ op.on('-b=VAL', '--bind-address=VAL', 'server bind address(default 0.0.0.0)') {|v| @params[:bind_address] = v }
40
+ op.on('--compile-config=VAL', '-c=VAL', 'mxmlc compile config ex:) --compile-config="-benchmark -strict=true"') do |v|
41
+ if @params[:compile_config]
42
+ @params[:compile_config] << ' ' << v
43
+ else
44
+ @params[:compile_config] = v
45
+ end
46
+ end
47
+
48
+ op.on('--fcsh-cmd=VAL', 'fcsh command path') {|v| @params[:fcsh_cmd] = v }
49
+ @params[:observe_files] = []
50
+ op.on('-I=VAL', '--observe-files=VAL', 'observe files and directories path') {|v| @params[:observe_files] << v }
51
+
52
+ if @params[:observe_files].empty?
53
+ @params[:observe_files] << '.'
54
+ end
55
+
56
+ op.on('-i=VAL', '--interval=VAL', 'interval time(min)') {|v| @params[:interval] = v.to_i }
57
+ op.on('-l=VAL', '--log=VAL', 'showing flashlog.txt') {|v| @params[:flashlog] = v }
58
+ op.on('-m=VAL', '--mapping=VAL', 'server mapping path :example) -m "../assets=assets" -m "../images/=img"') {|v|
59
+ @params[:mapping] ||= []
60
+ @params[:mapping] << v.split('=', 2)
61
+ }
62
+ op.on('--no-file-observe', "don't observing files") {|v| @params[:file_observing] = false }
63
+ op.on('--observe-ext=VAL', 'observe ext ex:) --observe-ext="as3,actionscript3,css,mxml"') {|v| @params[:ext] = v.split(',') }
64
+ op.on('--server', '-s', 'start autoreload webserver') {|v| @params[:server] = true }
65
+ op.on('--server-handler=val', 'set server hander :example) --server-handler=webrick') {|v| @params[:server] = v }
66
+ op.on('--port=val', '-p=val', 'server port(default: 3001)') {|v| @params[:port] = v.to_i }
67
+ op.on('--plugin=VAL', 'load plugin(s)') {|v|
68
+ @params[:plugin] ||= []
69
+ @params[:plugin] << v
70
+ }
71
+ op.on('-t=VAL', '--template=VAL', 'server use template file') {|v| @params[:template] = v }
72
+ op.on('-v', '--verbose', 'detail messages') {|v| @params[:logger].level = Logger::DEBUG }
73
+ op.on('--help', 'show this message') { puts op; Kernel::exit 1 }
74
+ op.parse! argv
75
+ @params[:logger].debug 'config' + @params.inspect
76
+ end
77
+
78
+ def merge_config(file)
79
+ @params.merge! YAML.load_file(file)
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,109 @@
1
+ begin
2
+ require 'expect'
3
+ rescue LoadError
4
+ require 'pathname'
5
+ require Pathname.new(__FILE__).parent.parent.parent.join('vendor/ruby/expect')
6
+ end
7
+ require 'thread'
8
+ require 'rascut/file_observer'
9
+
10
+ module Rascut
11
+ class FcshWrapper
12
+ FCSH_RESULT_RE = /fcsh: Assigned (\d+) as the compile target id/
13
+ FCSH_WAIT_RE = /^\(fcsh\)\s*$/
14
+
15
+ attr_accessor :original_files, :files, :config, :target_script, :hooks
16
+ def initialize(target_script, config)
17
+ @target_script = target_script
18
+ @config = config
19
+ @hooks = Hash.new {|h, k| h[k] = []}
20
+ @mutex = Mutex.new
21
+ @compile_mutex = Mutex.new
22
+ @compile_id = nil
23
+ @process = nil
24
+ @not_first_read = nil
25
+ end
26
+
27
+ def reload!
28
+ if @compile_id
29
+ process_sync_exec("clear #{@compile_id}")
30
+ @compile_id = nil
31
+ end
32
+ call_hook :reload, @compile_id
33
+ end
34
+
35
+ def process_sync_exec(str, result_get = true)
36
+ res = nil
37
+ @mutex.synchronize do
38
+ @process.puts str
39
+ res = read_result(@process) if result_get
40
+ end
41
+ res
42
+ end
43
+
44
+ def close
45
+ if @process
46
+ @process.close
47
+ call_hook :close
48
+ end
49
+ end
50
+
51
+ def logger
52
+ @config[:logger]
53
+ end
54
+
55
+ def mxmlc_cmd
56
+ cmd = ['mxmlc', @config[:compile_config], @target_script].join(' ')
57
+ logger.debug cmd
58
+ cmd
59
+ end
60
+
61
+ def compile
62
+ return false if @compile_mutex.locked?
63
+
64
+ @compile_mutex.synchronize do
65
+ logger.info "Compile Start"
66
+ out = nil
67
+ @process = IO.popen(@config[:fcsh_cmd] + ' 2>&1', 'r+') unless @process
68
+ if @compile_id
69
+ out = process_sync_exec "compile #{@compile_id}"
70
+ else
71
+ out = process_sync_exec mxmlc_cmd
72
+ if m = out.match(FCSH_RESULT_RE)
73
+ @compile_id = m[1]
74
+ else
75
+ raise "Can't get Compile ID\n" + out.to_s
76
+ end
77
+ end
78
+ logger.info out
79
+ if out.match(/bytes\)/)
80
+ call_hook :compile_success, out
81
+ else
82
+ call_hook :compile_error, out
83
+ end
84
+ call_hook :compile, out
85
+ end
86
+ end
87
+
88
+ def call_hook(name, *args)
89
+ @hooks[name].each do |hook|
90
+ if hook.arity == 0 || args.length == 0
91
+ hook.call
92
+ else
93
+ hook.call(*args)
94
+ end
95
+ end
96
+ end
97
+
98
+ def read_result(process)
99
+ unless @not_first_read
100
+ # first_time, FIXME uncool...
101
+ process.expect(FCSH_WAIT_RE)
102
+ @not_first_read = true
103
+ end
104
+
105
+ process.expect(FCSH_WAIT_RE).first.sub(FCSH_WAIT_RE, '')
106
+ end
107
+ end
108
+ end
109
+
@@ -0,0 +1,160 @@
1
+ require 'pathname'
2
+ require 'find'
3
+ require 'logger'
4
+
5
+ module Rascut
6
+ class FileObserver
7
+ DEFAULT_OPTIONS = {
8
+ :interval => 1,
9
+ :ignore_files => [],
10
+ :ignore_dirs => [/\/.svn/],
11
+ :logger => Logger.new(STDOUT),
12
+ :dir_counter => 5,
13
+ :ext => nil
14
+ }
15
+
16
+ def initialize(files, options)
17
+ @files = {}
18
+ @dirs = {}
19
+ @options = DEFAULT_OPTIONS.merge options
20
+ @update_handlers = []
21
+ @th = nil
22
+
23
+ if options[:update_handler]
24
+ add_update_handler options.delete(:update_handler)
25
+ end
26
+
27
+ observe files
28
+ end
29
+ attr_accessor :options
30
+
31
+ def logger
32
+ options[:logger]
33
+ end
34
+
35
+ def run
36
+ if @th
37
+ @th.run
38
+ else
39
+ @th = Thread.start do
40
+ loop do
41
+ update_check
42
+ sleep @options[:interval]
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ def add_update_handler(handler)
49
+ unless @update_handlers.include? handler
50
+ @update_handlers << handler
51
+ end
52
+ end
53
+
54
+ def remove_update_handler(handler)
55
+ @update_handlers.delete_if {|h| h == handler}
56
+ end
57
+
58
+ def stop
59
+ @th.kill
60
+ end
61
+
62
+ def observe(files)
63
+ Array(files).each do |file|
64
+ file = Pathname.new(file)
65
+ if file.directory?
66
+ dir_observe file
67
+ else
68
+ next if @options[:ignore_files].include?(file.realpath)
69
+
70
+ file_observe file
71
+ end
72
+ end
73
+ end
74
+
75
+ private
76
+
77
+ def update_check
78
+ update_files = []
79
+ check_dirs if check_dir?
80
+
81
+ @files.each do |file, mtime|
82
+ if !file.readable?
83
+ @files.delete file
84
+ elsif file.mtime > mtime
85
+ @files[file] = file.mtime
86
+ update_files << file
87
+ end
88
+ end
89
+
90
+ unless update_files.empty?
91
+ logger.info 'Found update file(s)' + update_files.map{|f| f.to_s}.inspect
92
+ @update_handlers.each do |handler|
93
+ if handler.arity == 1
94
+ handler.call update_files
95
+ else
96
+ handler.call
97
+ end
98
+ end
99
+ end
100
+ end
101
+
102
+ def check_dir?
103
+ @check_dir_count ||= options[:dir_counter]
104
+ if @check_dir_count > options[:dir_counter]
105
+ @check_dir_count = 0
106
+ else
107
+ @check_dir_count += 1
108
+ end
109
+ @check_dir_count.zero?
110
+ end
111
+
112
+ def check_dirs
113
+ dfiles = []
114
+ @dirs.each do |dir, mtime|
115
+ next if @options[:ignore_dirs].include?(dir.realpath)
116
+
117
+ if !dir.directory?
118
+ @dirs.delete dir
119
+ elsif dir.to_s.match %r{/\.svn|/CVS}
120
+ @dirs.delete dir
121
+ elsif dir.mtime > mtime
122
+ @dirs[dir] = dir.mtime
123
+
124
+ if @options[:ext]
125
+ e = '.{' + @options[:ext].join(',') + '}'
126
+ else
127
+ e = ''
128
+ end
129
+ dfiles.concat Pathname.glob(dir.to_s + "/{**/*}#{e}")
130
+ end
131
+ end
132
+ dfiles.uniq.each do |file|
133
+ if file.directory?
134
+ dir_observe file
135
+ else
136
+ file_observe file, Time.at(0)
137
+ end
138
+ end
139
+ end
140
+
141
+ def file_observe(file, mtime = nil)
142
+ if @options[:ext]
143
+ if @options[:ext].include? file.extname.sub(/^\./, '')
144
+ @files[file] ||= (mtime || file.mtime)
145
+ end
146
+ else
147
+ @files[file] ||= (mtime || file.mtime)
148
+ end
149
+ end
150
+
151
+ def dir_observe(dir)
152
+ Find::find(dir.to_s) do |file|
153
+ if File.directory?(file)
154
+ dir = Pathname.new(file)
155
+ @dirs[dir] ||= dir.mtime
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,304 @@
1
+
2
+ begin
3
+ require 'rack'
4
+ rescue LoadError
5
+ require 'rubygems'
6
+ gem 'rack'
7
+ end
8
+
9
+ require 'rack/showexceptions'
10
+ require 'rack/urlmap'
11
+ require 'rack/file'
12
+ require 'rack/request'
13
+ require 'rack/response'
14
+ require 'thread'
15
+ require 'logger'
16
+ require 'pathname'
17
+ require 'open-uri'
18
+
19
+ module Rascut
20
+ class Httpd
21
+ class FileOnly < Rack::File
22
+ def _call(env)
23
+ if env["PATH_INFO"].include? ".."
24
+ return [403, {"Content-Type" => "text/plain"}, ["Forbidden\n"]]
25
+ end
26
+
27
+ @path = env["PATH_INFO"] == '/' ? @root : F.join(@root, env['PATH_INFO'])
28
+ ext = F.extname(@path)[1..-1]
29
+
30
+ if F.file?(@path) && F.readable?(@path)
31
+ [200, {
32
+ "Content-Type" => MIME_TYPES[ext] || "text/plain",
33
+ "Content-Length" => F.size(@path).to_s
34
+ }, self]
35
+ else
36
+ return [404, {"Content-Type" => "text/plain"},
37
+ ["File not found: #{env["PATH_INFO"]}\n"]]
38
+ end
39
+ end
40
+ end
41
+
42
+ def initialize(command)
43
+ @command = command
44
+ @threads = []
45
+ end
46
+ attr_reader :command
47
+
48
+ def start
49
+ swf_path = command.root.to_s
50
+ logger.debug "swf_path: #{swf_path}"
51
+ vendor = Pathname.new(__FILE__).parent.parent.parent.join('vendor')
52
+ logger.debug "vendor_path: #{vendor}"
53
+ reload = reload_handler
54
+ index = index_handler
55
+
56
+ #app = Rack::FixedBuilder.new do
57
+ # use Rack::ShowExceptions
58
+ # map('/reload') { run reload }
59
+ # map('/swf/') { run Rack::File.new(swf_path) }
60
+ # map('/') { run index }
61
+ #end
62
+
63
+ urls = []
64
+ urls.concat(config_url_mapping) if config[:mapping]
65
+
66
+ urls.concat([
67
+ ['/js/swfobject.js', Rack::ShowExceptions.new(Httpd::FileOnly.new(vendor.join('js/swfobject.js').to_s))],
68
+ ['/swf', Rack::ShowExceptions.new(Rack::File.new(swf_path))],
69
+ ['/reload', Rack::ShowExceptions.new(reload_handler)],
70
+ ['/proxy', Rack::ShowExceptions.new(proxy_handler)],
71
+ ['/', Rack::ShowExceptions.new(index_handler)]
72
+ ])
73
+ logger.debug 'url mappings: ' + urls.map{|u| u.first}.inspect
74
+ app = Rack::URLMap.new(urls)
75
+ port = config[:port] || 3001
76
+ host = config[:bind_address] || '0.0.0.0'
77
+
78
+ _args = [app, {:Host => host, :Port => port}]
79
+ server_handler = detect_server
80
+ Thread.new(_args) do |args|
81
+ server_handler.run *args
82
+ end
83
+ logger.info "Start #{server_handler} http://#{host}:#{port}/"
84
+ end
85
+
86
+ def config
87
+ command.config
88
+ end
89
+
90
+ def logger
91
+ config.logger
92
+ end
93
+
94
+ def reload!
95
+ while t = @threads.pop
96
+ t.run
97
+ end
98
+ end
99
+
100
+ private
101
+ def config_url_mapping
102
+ urls = []
103
+ config[:mapping].each do |m|
104
+ filepath = m[0]
105
+ mappath = m[1]
106
+ mappath = '/' + mappath if mappath[0..0] != '/'
107
+ urls << [mappath, Rack::ShowExceptions.new(Rack::File.new(command.root.join(filepath).to_s))]
108
+ end
109
+ urls
110
+ end
111
+
112
+ def detect_server
113
+ begin
114
+ case config[:server]
115
+ when 'mongrel'
116
+ require_mongrel_handler
117
+ when 'webrick'
118
+ require_webrick_handler
119
+ else
120
+ require_mongrel_handler
121
+ end
122
+ rescue Exception => e
123
+ require_webrick_handler
124
+ end
125
+ end
126
+
127
+ def require_mongrel_handler
128
+ begin
129
+ require 'mongrel'
130
+ rescue LoadError
131
+ require 'rubygems'
132
+ gem 'mongrel', '> 1.0'
133
+ end
134
+ require 'rack/handler/mongrel'
135
+ Rack::Handler::Mongrel
136
+ end
137
+
138
+ def require_webrick_handler
139
+ require 'webrick'
140
+ require 'rack/handler/webrick'
141
+ Rack::Handler::WEBrick
142
+ end
143
+
144
+ def reload_handler
145
+ Proc.new do |env|
146
+ @threads << Thread.current
147
+ Thread.stop
148
+
149
+ logger.debug 'httpd /reload reloading now'
150
+ Rack::Response.new.finish do |r|
151
+ r.write '1'
152
+ end
153
+ end
154
+ end
155
+
156
+ def index_handler
157
+ if config[:template] && File.readable?(config[:template])
158
+ res = File.read(config[:template]) + "\n" + RELOAD_SCRIPT
159
+ else
160
+ res = INDEX.sub('__SWFOBJECT__', swftag(command.target_script, config)).sub('<!--__RELOAD__-->', RELOAD_SCRIPT)
161
+ end
162
+
163
+ Proc.new do |env|
164
+ req = Rack::Request.new(env)
165
+ res.sub!('__SWF_VARS__', swfvars(req.GET))
166
+ Rack::Response.new.finish do |r|
167
+ r.write res
168
+ end
169
+ end
170
+ end
171
+
172
+ def proxy_handler
173
+ Proc.new do |env|
174
+ req = Rack::Request.new(env)
175
+ url = req.query_string
176
+ if url.empty?
177
+ url = req.path_info[1..-1].gsub(%r{^(https?:/)/?}, '\\1/')
178
+ end
179
+ Rack::Response.new.finish do |r|
180
+ open(url) { |io|
181
+ r['Content-Type'] = io.content_type
182
+ while part = io.read(8192)
183
+ r.write part
184
+ end
185
+ }
186
+ end
187
+ end
188
+ end
189
+
190
+ def swfvars(vars)
191
+ res = []
192
+ vars.each do |key, value|
193
+ res << %Q[so.addVariable('#{key}', '#{value}');]
194
+ end
195
+ res.join("\n")
196
+ end
197
+
198
+ def swftag(target_script, options)
199
+ name = output_file(options[:compile_config]) || target_script
200
+ name = File.basename(name, '.*')
201
+ swf = "/swf/#{name}.swf"
202
+ height, width = wh_parse options[:compile_config]
203
+ bgcol = bg_parse options[:compile_config]
204
+ # %Q[new SWFObject("#{swf}?#{Time.now.to_i}#{Time.now.usec}", "#{name}", "#{width}", "#{height}", '9', '#{bgcol}');]
205
+ %Q[new SWFObject("#{swf}?" + (new Date()).getTime(), "idswf", "#{width}", "#{height}", '9', '#{bgcol}');]
206
+ end
207
+
208
+ def bg_parse(opt)
209
+ if m = opt.to_s.match(/-default-background-color=0x([a-fA-F0-9]{3,6})/)
210
+ '#' + m[1]
211
+ else
212
+ '#ffffff'
213
+ end
214
+ end
215
+
216
+ def wh_parse(opt)
217
+ if m = opt.to_s.match(/-default-size\s+(\d+)\s+(\d+)/)
218
+ m.to_a[1..2]
219
+ else
220
+ ['100%', '100%']
221
+ end
222
+ end
223
+
224
+ def output_file(opt)
225
+ if m = opt.to_s.match(/-(o|output)\s+([^\s]+)/)
226
+ m[2]
227
+ else
228
+ nil
229
+ end
230
+ end
231
+
232
+ INDEX = <<-EOF
233
+ <html>
234
+ <head>
235
+ <title>Rascut</title>
236
+ <style>
237
+ * {
238
+ margin:0;
239
+ padding:0;
240
+ }
241
+ #content {
242
+ text-align:center;
243
+ }
244
+ </style>
245
+ <script type="text/javascript" src="/js/swfobject.js"></script>
246
+ <!--__RELOAD__-->
247
+ </head>
248
+ <body>
249
+ <div id="content">
250
+ </div>
251
+
252
+ <script type="text/javascript">
253
+ var so = __SWFOBJECT__;
254
+ __SWF_VARS__
255
+ so.addVariable('rascut', 'true');
256
+ so.write("content");
257
+ </script>
258
+ </body>
259
+ </html>
260
+ EOF
261
+
262
+ RELOAD_SCRIPT = <<-EOF
263
+ <script type="text/javascript">
264
+ var Rascut = new Object;
265
+
266
+ Rascut.xhr = (function() {
267
+ if (typeof XMLHttpRequest != 'undefined') {
268
+ return new XMLHttpRequest();
269
+ } else {
270
+ try {
271
+ return new ActiveXObject("Msxml2.XMLHTTP");
272
+ } catch(e) {
273
+ return new ActiveXObject("Microsoft.XMLHTTP");
274
+ }
275
+ }
276
+ })();
277
+
278
+ Rascut.reloadObserver = function() {
279
+ var x = Rascut.xhr;
280
+ x.open('GET', '/reload?' + (new Date()).getTime(), true);
281
+ x.onreadystatechange = function() {
282
+ try {
283
+ if (x.readyState == 4) {
284
+ if (x.status == 200 && Number(x.responseText) == 1) {
285
+ // thanks os0x!
286
+ so.attributes.swf = so.attributes.swf + '+';
287
+ so.write('content');
288
+ Rascut.reloadObserver();
289
+ } else {
290
+ setTimeout(Rascut.reloadObserver, 5000);
291
+ }
292
+ }
293
+ } catch(e) {
294
+ setTimeout(Rascut.reloadObserver, 5000);
295
+ }
296
+ }
297
+ x.send(null);
298
+ }
299
+
300
+ Rascut.reloadObserver();
301
+ </script>
302
+ EOF
303
+ end
304
+ end
@@ -0,0 +1,18 @@
1
+ require 'logger'
2
+
3
+ module Rascut
4
+ class Logger < ::Logger
5
+ # This code from ActiveSupport
6
+ private
7
+ alias old_format_message format_message
8
+ if method_defined?(:formatter=)
9
+ def format_message(severity, timestamp, progname, msg)
10
+ "[#{Time.now.strftime('%m/%d %H:%M:%S')}] #{msg}\n"
11
+ end
12
+ else
13
+ def format_message(severity, timestamp, msg, progname)
14
+ "[#{Time.now.strftime('%m/%d %H:%M:%S')}] #{msg}\n"
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,19 @@
1
+
2
+ module Rascut
3
+ module Plugin
4
+ class Base
5
+ def initialize(command)
6
+ @command = command
7
+ end
8
+
9
+ def config
10
+ # ...
11
+ {}
12
+ end
13
+
14
+ def run
15
+ raise 'should\'d be run method override!'
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,31 @@
1
+
2
+ require 'rascut/plugin/base'
3
+ require 'pathname'
4
+
5
+ module Rascut
6
+ module Plugin
7
+ class WriteFcshErrorOutput < Base
8
+ def run
9
+ file = config[:filename] || Pathname.new(ENV['HOME']).join('.rascut/error_output')
10
+ @path = Pathname.new(file.to_s)
11
+
12
+ @command.wrapper.hooks[:compile_error] << method(:write_error_output)
13
+ @command.wrapper.hooks[:compile_success] << method(:write_error_none)
14
+ end
15
+
16
+ def write_error_output(str)
17
+ str.each_line do |line|
18
+ if line.match 'Error: '
19
+ @path.open('w'){|f| f.puts line.chomp }
20
+ break
21
+ end
22
+ end
23
+ end
24
+
25
+ def write_error_none(str)
26
+ @path.open('w'){|f| f.write '' }
27
+ end
28
+
29
+ end
30
+ end
31
+ end
data/lib/rascut.rb ADDED
@@ -0,0 +1,9 @@
1
+
2
+ module Rascut
3
+ VERSION = '0.1.0'
4
+ end
5
+
6
+ require 'rascut/logger'
7
+ require 'rascut/fcsh_wrapper'
8
+ require 'rascut/command'
9
+
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH << File.expand_path(File.dirname(__FILE__) + "/../lib")
3
+ require 'rascut/file_observer'
4
+ require 'test/unit'
5
+ require 'tmpdir'
6
+ require 'pathname'
7
+ $DEBUG = true
8
+
9
+ class FileObserverTest < Test::Unit::TestCase
10
+ def setup
11
+ @tmpdir = Pathname.new(Dir.tmpdir).join('test_file_observer_' + Time.now.to_i.to_s)
12
+ @tmpdir.mkpath
13
+
14
+ @fo = Rascut::FileObserver.new(@tmpdir.to_s, :interval => 1, :dir_counter => 0)
15
+ @update_files = []
16
+
17
+ @fo.add_update_handler method(:save_files)
18
+
19
+ @call_update_handler_flag = false
20
+ @fo.add_update_handler method(:call_update_handler)
21
+
22
+ @fo.run
23
+ end
24
+
25
+ def call_update_handler(files)
26
+ @call_update_handler_flag = true
27
+ end
28
+
29
+ def save_files(files)
30
+ @update_files << files
31
+ end
32
+
33
+ def test_dir
34
+ sleep 1.1
35
+ @tmpdir.join('foo').mkpath
36
+ sleep 1.1
37
+ assert !@call_update_handler_flag
38
+
39
+ @tmpdir.join('foo/foo.txt').open('w') {|f| f.puts 'file'}
40
+ sleep 1.1
41
+ assert @call_update_handler_flag
42
+ @call_update_handler_flag = false
43
+
44
+ @tmpdir.join('foo/foo').mkpath
45
+ sleep 1.1
46
+ assert !@call_update_handler_flag
47
+
48
+ @tmpdir.join('foo/foo/foo.txt').open('w') {|f| f.puts 'file'}
49
+ sleep 1.1
50
+ assert @call_update_handler_flag
51
+ end
52
+
53
+ def test_fileput
54
+ sleep 1
55
+ @tmpdir.join('foo.txt').open('w') {|f| f.puts 'file'}
56
+ sleep 1
57
+ assert @call_update_handler_flag
58
+
59
+ @call_update_handler_flag = false
60
+ sleep 1
61
+ @tmpdir.join('foo.txt').open('w+') {|f| f.puts 'file'}
62
+ sleep 1
63
+ assert @call_update_handler_flag
64
+
65
+ @call_update_handler_flag = false
66
+ sleep 1
67
+ assert(!@call_update_handler_flag)
68
+ end
69
+
70
+ def test_fileput_with_ext
71
+ @fo.options[:ext] = ['txt']
72
+ sleep 1
73
+ @tmpdir.join('foo.txt').open('w') {|f| f.puts 'file'}
74
+ sleep 1
75
+ assert @call_update_handler_flag
76
+ end
77
+
78
+ def test_fileput_with_ext_nomatch
79
+ @fo.options[:ext] = ['nontext']
80
+ sleep 1
81
+ @tmpdir.join('foo.txt').open('w') {|f| f.puts 'file'}
82
+ sleep 1
83
+ assert !@call_update_handler_flag
84
+ end
85
+
86
+ def teardown
87
+ @fo.stop
88
+ @tmpdir.rmtree
89
+ end
90
+ end
File without changes
@@ -0,0 +1,8 @@
1
+ /**
2
+ * SWFObject v1.5: Flash Player detection and embed - http://blog.deconcept.com/swfobject/
3
+ *
4
+ * SWFObject is (c) 2007 Geoff Stearns and is released under the MIT License:
5
+ * http://www.opensource.org/licenses/mit-license.php
6
+ *
7
+ */
8
+ if(typeof deconcept=="undefined"){var deconcept=new Object();}if(typeof deconcept.util=="undefined"){deconcept.util=new Object();}if(typeof deconcept.SWFObjectUtil=="undefined"){deconcept.SWFObjectUtil=new Object();}deconcept.SWFObject=function(_1,id,w,h,_5,c,_7,_8,_9,_a){if(!document.getElementById){return;}this.DETECT_KEY=_a?_a:"detectflash";this.skipDetect=deconcept.util.getRequestParameter(this.DETECT_KEY);this.params=new Object();this.variables=new Object();this.attributes=new Array();if(_1){this.setAttribute("swf",_1);}if(id){this.setAttribute("id",id);}if(w){this.setAttribute("width",w);}if(h){this.setAttribute("height",h);}if(_5){this.setAttribute("version",new deconcept.PlayerVersion(_5.toString().split(".")));}this.installedVer=deconcept.SWFObjectUtil.getPlayerVersion();if(!window.opera&&document.all&&this.installedVer.major>7){deconcept.SWFObject.doPrepUnload=true;}if(c){this.addParam("bgcolor",c);}var q=_7?_7:"high";this.addParam("quality",q);this.setAttribute("useExpressInstall",false);this.setAttribute("doExpressInstall",false);var _c=(_8)?_8:window.location;this.setAttribute("xiRedirectUrl",_c);this.setAttribute("redirectUrl","");if(_9){this.setAttribute("redirectUrl",_9);}};deconcept.SWFObject.prototype={useExpressInstall:function(_d){this.xiSWFPath=!_d?"expressinstall.swf":_d;this.setAttribute("useExpressInstall",true);},setAttribute:function(_e,_f){this.attributes[_e]=_f;},getAttribute:function(_10){return this.attributes[_10];},addParam:function(_11,_12){this.params[_11]=_12;},getParams:function(){return this.params;},addVariable:function(_13,_14){this.variables[_13]=_14;},getVariable:function(_15){return this.variables[_15];},getVariables:function(){return this.variables;},getVariablePairs:function(){var _16=new Array();var key;var _18=this.getVariables();for(key in _18){_16[_16.length]=key+"="+_18[key];}return _16;},getSWFHTML:function(){var _19="";if(navigator.plugins&&navigator.mimeTypes&&navigator.mimeTypes.length){if(this.getAttribute("doExpressInstall")){this.addVariable("MMplayerType","PlugIn");this.setAttribute("swf",this.xiSWFPath);}_19="<embed type=\"application/x-shockwave-flash\" src=\""+this.getAttribute("swf")+"\" width=\""+this.getAttribute("width")+"\" height=\""+this.getAttribute("height")+"\" style=\""+this.getAttribute("style")+"\"";_19+=" id=\""+this.getAttribute("id")+"\" name=\""+this.getAttribute("id")+"\" ";var _1a=this.getParams();for(var key in _1a){_19+=[key]+"=\""+_1a[key]+"\" ";}var _1c=this.getVariablePairs().join("&");if(_1c.length>0){_19+="flashvars=\""+_1c+"\"";}_19+="/>";}else{if(this.getAttribute("doExpressInstall")){this.addVariable("MMplayerType","ActiveX");this.setAttribute("swf",this.xiSWFPath);}_19="<object id=\""+this.getAttribute("id")+"\" classid=\"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000\" width=\""+this.getAttribute("width")+"\" height=\""+this.getAttribute("height")+"\" style=\""+this.getAttribute("style")+"\">";_19+="<param name=\"movie\" value=\""+this.getAttribute("swf")+"\" />";var _1d=this.getParams();for(var key in _1d){_19+="<param name=\""+key+"\" value=\""+_1d[key]+"\" />";}var _1f=this.getVariablePairs().join("&");if(_1f.length>0){_19+="<param name=\"flashvars\" value=\""+_1f+"\" />";}_19+="</object>";}return _19;},write:function(_20){if(this.getAttribute("useExpressInstall")){var _21=new deconcept.PlayerVersion([6,0,65]);if(this.installedVer.versionIsValid(_21)&&!this.installedVer.versionIsValid(this.getAttribute("version"))){this.setAttribute("doExpressInstall",true);this.addVariable("MMredirectURL",escape(this.getAttribute("xiRedirectUrl")));document.title=document.title.slice(0,47)+" - Flash Player Installation";this.addVariable("MMdoctitle",document.title);}}if(this.skipDetect||this.getAttribute("doExpressInstall")||this.installedVer.versionIsValid(this.getAttribute("version"))){var n=(typeof _20=="string")?document.getElementById(_20):_20;n.innerHTML=this.getSWFHTML();return true;}else{if(this.getAttribute("redirectUrl")!=""){document.location.replace(this.getAttribute("redirectUrl"));}}return false;}};deconcept.SWFObjectUtil.getPlayerVersion=function(){var _23=new deconcept.PlayerVersion([0,0,0]);if(navigator.plugins&&navigator.mimeTypes.length){var x=navigator.plugins["Shockwave Flash"];if(x&&x.description){_23=new deconcept.PlayerVersion(x.description.replace(/([a-zA-Z]|\s)+/,"").replace(/(\s+r|\s+b[0-9]+)/,".").split("."));}}else{if(navigator.userAgent&&navigator.userAgent.indexOf("Windows CE")>=0){var axo=1;var _26=3;while(axo){try{_26++;axo=new ActiveXObject("ShockwaveFlash.ShockwaveFlash."+_26);_23=new deconcept.PlayerVersion([_26,0,0]);}catch(e){axo=null;}}}else{try{var axo=new ActiveXObject("ShockwaveFlash.ShockwaveFlash.7");}catch(e){try{var axo=new ActiveXObject("ShockwaveFlash.ShockwaveFlash.6");_23=new deconcept.PlayerVersion([6,0,21]);axo.AllowScriptAccess="always";}catch(e){if(_23.major==6){return _23;}}try{axo=new ActiveXObject("ShockwaveFlash.ShockwaveFlash");}catch(e){}}if(axo!=null){_23=new deconcept.PlayerVersion(axo.GetVariable("$version").split(" ")[1].split(","));}}}return _23;};deconcept.PlayerVersion=function(_29){this.major=_29[0]!=null?parseInt(_29[0]):0;this.minor=_29[1]!=null?parseInt(_29[1]):0;this.rev=_29[2]!=null?parseInt(_29[2]):0;};deconcept.PlayerVersion.prototype.versionIsValid=function(fv){if(this.major<fv.major){return false;}if(this.major>fv.major){return true;}if(this.minor<fv.minor){return false;}if(this.minor>fv.minor){return true;}if(this.rev<fv.rev){return false;}return true;};deconcept.util={getRequestParameter:function(_2b){var q=document.location.search||document.location.hash;if(_2b==null){return q;}if(q){var _2d=q.substring(1).split("&");for(var i=0;i<_2d.length;i++){if(_2d[i].substring(0,_2d[i].indexOf("="))==_2b){return _2d[i].substring((_2d[i].indexOf("=")+1));}}}return "";}};deconcept.SWFObjectUtil.cleanupSWFs=function(){var _2f=document.getElementsByTagName("OBJECT");for(var i=_2f.length-1;i>=0;i--){_2f[i].style.display="none";for(var x in _2f[i]){if(typeof _2f[i][x]=="function"){_2f[i][x]=function(){};}}}};if(deconcept.SWFObject.doPrepUnload){if(!deconcept.unloadSet){deconcept.SWFObjectUtil.prepUnload=function(){__flash_unloadHandler=function(){};__flash_savedUnloadHandler=function(){};window.attachEvent("onunload",deconcept.SWFObjectUtil.cleanupSWFs);};window.attachEvent("onbeforeunload",deconcept.SWFObjectUtil.prepUnload);deconcept.unloadSet=true;}}if(!document.getElementById&&document.all){document.getElementById=function(id){return document.all[id];};}var getQueryParamValue=deconcept.util.getRequestParameter;var FlashObject=deconcept.SWFObject;var SWFObject=deconcept.SWFObject;
@@ -0,0 +1,36 @@
1
+ $expect_verbose = false
2
+
3
+ class IO
4
+ def expect(pat,timeout=9999999)
5
+ buf = ''
6
+ case pat
7
+ when String
8
+ e_pat = Regexp.new(Regexp.quote(pat))
9
+ when Regexp
10
+ e_pat = pat
11
+ end
12
+ while true
13
+ if IO.select([self],nil,nil,timeout).nil? then
14
+ result = nil
15
+ break
16
+ end
17
+ c = getc.chr
18
+ buf << c
19
+ if $expect_verbose
20
+ STDOUT.print c
21
+ STDOUT.flush
22
+ end
23
+ if mat=e_pat.match(buf) then
24
+ result = [buf,*mat.to_a[1..-1]]
25
+ break
26
+ end
27
+ end
28
+ if block_given? then
29
+ yield result
30
+ else
31
+ return result
32
+ end
33
+ nil
34
+ end
35
+ end
36
+
metadata ADDED
@@ -0,0 +1,111 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.9.1
3
+ specification_version: 1
4
+ name: rascut
5
+ version: !ruby/object:Gem::Version
6
+ version: 0.1.0
7
+ date: 2007-09-02 00:00:00 +09:00
8
+ summary: Ruby ActionSCript UTility
9
+ require_paths:
10
+ - lib
11
+ email: hotchpotch@nononospam@gmail.com
12
+ homepage: " by Yuichi Tateno"
13
+ rubyforge_project: hotchpotch
14
+ description: "== FEATURES/PROBLEMS: == SYNOPSIS: * 1. flex2sdk and fcwrap setting. * 2. $ rascut -s MyScript.as * 3. Browser access http://localhost:3001/ == REQUIREMENTS:"
15
+ autorequire:
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ post_install_message:
29
+ authors:
30
+ - yuichi tateno
31
+ files:
32
+ - History.txt
33
+ - Manifest.txt
34
+ - README.txt
35
+ - Rakefile
36
+ - bin/rascut
37
+ - lib/rascut.rb
38
+ - lib/rascut/command.rb
39
+ - lib/rascut/config.rb
40
+ - lib/rascut/fcsh_wrapper.rb
41
+ - lib/rascut/file_observer.rb
42
+ - lib/rascut/httpd.rb
43
+ - lib/rascut/logger.rb
44
+ - lib/rascut/plugin/base.rb
45
+ - lib/rascut/plugin/write_fcsh_error_output.rb
46
+ - vendor/ruby/expect.rb
47
+ - vendor/js/swfobject.js
48
+ - test/test_rascut.rb
49
+ - test/test_file_observer.rb
50
+ test_files:
51
+ - test/test_rascut.rb
52
+ - test/test_file_observer.rb
53
+ rdoc_options:
54
+ - --main
55
+ - README.txt
56
+ extra_rdoc_files:
57
+ - History.txt
58
+ - Manifest.txt
59
+ - README.txt
60
+ executables:
61
+ - rascut
62
+ extensions: []
63
+
64
+ requirements: []
65
+
66
+ dependencies:
67
+ - !ruby/object:Gem::Dependency
68
+ name: mongrel
69
+ version_requirement:
70
+ version_requirements: !ruby/object:Gem::Version::Requirement
71
+ requirements:
72
+ - - ">"
73
+ - !ruby/object:Gem::Version
74
+ version: 0.0.0
75
+ version:
76
+ - !ruby/object:Gem::Dependency
77
+ name: rake
78
+ version_requirement:
79
+ version_requirements: !ruby/object:Gem::Version::Requirement
80
+ requirements:
81
+ - - ">"
82
+ - !ruby/object:Gem::Version
83
+ version: 0.0.0
84
+ version:
85
+ - !ruby/object:Gem::Dependency
86
+ name: hoe
87
+ version_requirement:
88
+ version_requirements: !ruby/object:Gem::Version::Requirement
89
+ requirements:
90
+ - - ">"
91
+ - !ruby/object:Gem::Version
92
+ version: 0.0.0
93
+ version:
94
+ - !ruby/object:Gem::Dependency
95
+ name: rack
96
+ version_requirement:
97
+ version_requirements: !ruby/object:Gem::Version::Requirement
98
+ requirements:
99
+ - - ">"
100
+ - !ruby/object:Gem::Version
101
+ version: 0.0.0
102
+ version:
103
+ - !ruby/object:Gem::Dependency
104
+ name: hoe
105
+ version_requirement:
106
+ version_requirements: !ruby/object:Gem::Version::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: 1.3.0
111
+ version: