boson-more 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. data/.gemspec +22 -0
  2. data/LICENSE.txt +22 -0
  3. data/README.md +97 -0
  4. data/Rakefile +35 -0
  5. data/deps.rip +1 -0
  6. data/lib/boson/alias.rb +75 -0
  7. data/lib/boson/argument_inspector.rb +90 -0
  8. data/lib/boson/commands/core.rb +67 -0
  9. data/lib/boson/commands/view_core.rb +19 -0
  10. data/lib/boson/commands/web_core.rb +153 -0
  11. data/lib/boson/comment_inspector.rb +100 -0
  12. data/lib/boson/console.rb +40 -0
  13. data/lib/boson/console_runner.rb +60 -0
  14. data/lib/boson/index.rb +48 -0
  15. data/lib/boson/libraries/file_library.rb +144 -0
  16. data/lib/boson/libraries/gem_library.rb +30 -0
  17. data/lib/boson/libraries/local_file_library.rb +30 -0
  18. data/lib/boson/libraries/module_library.rb +37 -0
  19. data/lib/boson/libraries/require_library.rb +23 -0
  20. data/lib/boson/libraries.rb +183 -0
  21. data/lib/boson/more/version.rb +5 -0
  22. data/lib/boson/more.rb +18 -0
  23. data/lib/boson/more_commands.rb +14 -0
  24. data/lib/boson/more_inspector.rb +42 -0
  25. data/lib/boson/more_manager.rb +34 -0
  26. data/lib/boson/more_method_inspector.rb +74 -0
  27. data/lib/boson/more_option_parser.rb +28 -0
  28. data/lib/boson/more_scientist.rb +68 -0
  29. data/lib/boson/more_util.rb +30 -0
  30. data/lib/boson/namespace.rb +31 -0
  31. data/lib/boson/namespacer.rb +117 -0
  32. data/lib/boson/pipe.rb +156 -0
  33. data/lib/boson/pipe_runner.rb +44 -0
  34. data/lib/boson/pipes.rb +75 -0
  35. data/lib/boson/repo.rb +96 -0
  36. data/lib/boson/repo_index.rb +135 -0
  37. data/lib/boson/runner_options.rb +88 -0
  38. data/lib/boson/save.rb +198 -0
  39. data/lib/boson/science.rb +273 -0
  40. data/lib/boson/view.rb +98 -0
  41. data/lib/boson/viewable.rb +48 -0
  42. data/test/alias_test.rb +55 -0
  43. data/test/argument_inspector_test.rb +40 -0
  44. data/test/command_test.rb +22 -0
  45. data/test/commands_test.rb +53 -0
  46. data/test/comment_inspector_test.rb +126 -0
  47. data/test/console_runner_test.rb +58 -0
  48. data/test/deps.rip +4 -0
  49. data/test/file_library_test.rb +41 -0
  50. data/test/gem_library_test.rb +40 -0
  51. data/test/libraries_test.rb +55 -0
  52. data/test/loader_test.rb +38 -0
  53. data/test/module_library_test.rb +30 -0
  54. data/test/more_manager_test.rb +29 -0
  55. data/test/more_method_inspector_test.rb +42 -0
  56. data/test/more_scientist_test.rb +10 -0
  57. data/test/namespacer_test.rb +61 -0
  58. data/test/pipes_test.rb +65 -0
  59. data/test/repo_index_test.rb +123 -0
  60. data/test/repo_test.rb +23 -0
  61. data/test/runner_options_test.rb +29 -0
  62. data/test/save_test.rb +86 -0
  63. data/test/science_test.rb +58 -0
  64. data/test/scientist_test.rb +195 -0
  65. data/test/test_helper.rb +165 -0
  66. data/test/web_test.rb +22 -0
  67. metadata +169 -0
data/.gemspec ADDED
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require 'rubygems' unless Object.const_defined?(:Gem)
3
+ require File.dirname(__FILE__) + "/lib/boson/more/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "boson-more"
7
+ s.version = Boson::More::VERSION
8
+ s.authors = ["Gabriel Horner"]
9
+ s.email = "gabriel.horner@gmail.com"
10
+ s.homepage = "http://github.com/cldwalker/boson-more"
11
+ s.summary = "boson2 plugins"
12
+ s.description = "A collection of boson plugins that can be mixed and matched"
13
+ s.required_rubygems_version = ">= 1.3.6"
14
+ s.add_dependency 'boson', '>= 1.0.0'
15
+ s.add_development_dependency 'mocha'
16
+ s.add_development_dependency 'bacon', '>= 1.1.0'
17
+ s.add_development_dependency 'mocha-on-bacon'
18
+ s.add_development_dependency 'bacon-bits'
19
+ s.files = Dir.glob(%w[{lib,test}/**/*.rb bin/* [A-Z]*.{txt,rdoc} ext/**/*.{rb,c} **/deps.rip]) + %w{Rakefile .gemspec}
20
+ s.extra_rdoc_files = ["README.md", "LICENSE.txt"]
21
+ s.license = 'MIT'
22
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ The MIT LICENSE
2
+
3
+ Copyright (c) 2010 Gabriel Horner
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,97 @@
1
+ ## Description
2
+
3
+ boson-more is a collection of useful boson plugins. These plugins add features
4
+ such as allowing boson to be used from irb, optional automated views generated
5
+ by hirb and allowing libraries to be written as plain ruby. For my libraries
6
+ that use this, see [irbfiles](http://github.com/cldwalker/irbfiles).
7
+
8
+ ## Usage
9
+
10
+ To use all of the plugins, add this to ~/.bosonrc:
11
+
12
+ require 'boson/more'
13
+
14
+ To only use certain plugins, require those plugins in ~/.bosonrc.
15
+
16
+ When using all plugins, you can use boson in irb/ripl by dropping this in ~/.irbrc:
17
+
18
+ require 'boson'
19
+ Boson.start
20
+
21
+
22
+ ## List of Plugins
23
+
24
+ * boson/alias - Adds aliasing to commands. Requires alias gem.
25
+ * boson/console - Allows for boson to be used in a ruby console.
26
+ * boson/libraries - Adds several libraries. Necessary for using old boson.
27
+ * boson/more\_commands - Adds set of default commands
28
+ * boson/more\_inspector - Adds commenting-based command configuration.
29
+ * boson/more\_manager - Adds ability for libraries to have dependencies.
30
+ * boson/more\_method\_inspector - Adds ability to scrape argument names and
31
+ default values from commands.
32
+ * boson/more\_scientist - Adds a few global options to option commands.
33
+ * boson/runner\_options - Adds additional options to the boson executable.
34
+ * boson/science - Adds pipes and several global options to option commands.
35
+ Requires hirb.
36
+ * boson/namespacer - Adds namespaces to commands.
37
+ * boson/save - Allows libraries and commands to be saved and loaded quickly.
38
+ Necessary for using old boson.
39
+ * boson/viewable - Adds rendering to commands. Requires hirb.
40
+
41
+ ## Features
42
+
43
+ When using all these plugins, they have the following features:
44
+
45
+ * Simple organization: Commands are just methods on an object (default is main)
46
+ and command libraries are just modules.
47
+ * Commands are accessible from the commandline (Boson::BinRunner) or irb
48
+ (Boson::ConsoleRunner).
49
+ * Libraries
50
+ * can be written in plain ruby which allows for easy testing and use
51
+ independent of boson (Boson::FileLibrary).
52
+ * can exist locally as a Bosonfile (Boson::LocalFileLibrary) and under
53
+ lib/boson/commands or .boson/commands.
54
+ * can be made from gems (Boson::GemLibrary) or any require-able file
55
+ (Boson::RequireLibrary).
56
+ * are encouraged to be shared. Libraries can be installed with a given url.
57
+ Users can customize any aspect of a third-party library without modifying it
58
+ (Boson::Library).
59
+ * Commands
60
+ * can have any number of local and global options (Boson::OptionCommand).
61
+ Options are defined with Boson::OptionParser.
62
+ * can have any view associated to it (via Hirb) without adding view code to
63
+ the command's method. These views can be toggled on and manipulated via
64
+ global render options (Boson::View and Boson::OptionCommand).
65
+ * can pipe their return value into custom pipe options (Boson::Pipe).
66
+ * has default pipe options to search and sort an array of any objects
67
+ (Boson::Pipes).
68
+ * Option parser (Boson::OptionParser)
69
+ * provides option types that map to objects i.e. :array type creates Array
70
+ objects.
71
+ * come with 5 default option types: boolean, array, string, hash and numeric.
72
+ * can have have custom option types defined by users (Boson::Options).
73
+ * Comes with default commands to load, search, list and install commands and
74
+ libraries (Boson::Commands::Core).
75
+ * Namespaces are optional and when used are methods which allow for
76
+ method\_missing magic.
77
+
78
+
79
+ ## Bugs/Issues
80
+
81
+ Please report them [on github](http://github.com/cldwalker/boson-more/issues).
82
+
83
+ ## Contributing
84
+
85
+ [See here](http://tagaholic.me/contributing.html)
86
+
87
+ ## Links
88
+
89
+ * http://tagaholic.me/2009/10/14/boson-command-your-ruby-universe.html
90
+ * http://tagaholic.me/2009/10/15/boson-and-hirb-interactions.html
91
+ * http://tagaholic.me/2009/10/19/how-boson-enhances-your-irb-experience.html
92
+ ## TODO
93
+
94
+ * Actually have working tests
95
+ * Clean up plugins and move their files into separates directories
96
+ * Clean up plugins that unintentionally depend on each other
97
+ * Clean up docs which are currently strewn across plugins
data/Rakefile ADDED
@@ -0,0 +1,35 @@
1
+ require 'rake'
2
+ require 'fileutils'
3
+
4
+ def gemspec
5
+ @gemspec ||= eval(File.read('.gemspec'), binding, '.gemspec')
6
+ end
7
+
8
+ desc "Build the gem"
9
+ task :gem=>:gemspec do
10
+ sh "gem build .gemspec"
11
+ FileUtils.mkdir_p 'pkg'
12
+ FileUtils.mv "#{gemspec.name}-#{gemspec.version}.gem", 'pkg'
13
+ end
14
+
15
+ desc "Install the gem locally"
16
+ task :install => :gem do
17
+ sh %{gem install pkg/#{gemspec.name}-#{gemspec.version}}
18
+ end
19
+
20
+ desc "Generate the gemspec"
21
+ task :generate do
22
+ puts gemspec.to_ruby
23
+ end
24
+
25
+ desc "Validate the gemspec"
26
+ task :gemspec do
27
+ gemspec.validate
28
+ end
29
+
30
+ desc 'Run tests'
31
+ task :test do |t|
32
+ sh 'bacon -q -Ilib -I. test/*_test.rb'
33
+ end
34
+
35
+ task :default => :test
data/deps.rip ADDED
@@ -0,0 +1 @@
1
+ boson >=1.0.0
@@ -0,0 +1,75 @@
1
+ require 'alias'
2
+
3
+ module Boson
4
+ class Library
5
+ # [*:class_commands*] A hash of commands to create. A hash key-pair can map command names to any string of ruby code
6
+ # that ends with a method call. Or a key-pair can map a class to an array of its class methods
7
+ # to create commands of the same name. Example:
8
+ # :class_commands=>{'spy'=>'Bond.spy', 'create'=>'Alias.manager.create',
9
+ # 'Boson::Util'=>['detect', 'any_const_get']}
10
+ # [*:no_alias_creation*] Boolean which doesn't create aliases for a library. Useful for libraries that configure command
11
+ # aliases outside of Boson's control. Default is false.
12
+ module Alias
13
+ attr_reader :class_commands, :no_alias_creation
14
+ end
15
+ include Alias
16
+
17
+ module AliasLoader
18
+ def load_commands?
19
+ super || @class_commands
20
+ end
21
+
22
+ def before_load_commands
23
+ unless @class_commands.nil? || @class_commands.empty? || @method_conflict
24
+ Boson::Manager.create_class_aliases(@module, @class_commands)
25
+ end
26
+ super
27
+ end
28
+ end
29
+ include AliasLoader
30
+ end
31
+
32
+ class Manager
33
+ def self.create_class_aliases(mod, class_commands)
34
+ class_commands.dup.each {|k,v|
35
+ if v.is_a?(Array)
36
+ class_commands.delete(k).each {|e| class_commands[e] = "#{k}.#{e}"}
37
+ end
38
+ }
39
+ Alias.manager.create_aliases(:any_to_instance_method, mod.to_s=>class_commands.invert)
40
+ end
41
+
42
+ module AliasLib
43
+ def after_create_commands(lib, commands)
44
+ create_command_aliases(lib, commands) if commands.size > 0 && !lib.no_alias_creation
45
+ end
46
+
47
+ def create_command_aliases(lib, commands)
48
+ lib.module ? prep_and_create_instance_aliases(commands, lib.module) : check_for_uncreated_aliases(lib, commands)
49
+ end
50
+
51
+ def prep_and_create_instance_aliases(commands, lib_module)
52
+ aliases_hash = {}
53
+ select_commands = Boson.commands.select {|e| commands.include?(e.name)}
54
+ select_commands.each do |e|
55
+ if e.alias
56
+ aliases_hash[lib_module.to_s] ||= {}
57
+ aliases_hash[lib_module.to_s][e.name] = e.alias
58
+ end
59
+ end
60
+ create_instance_aliases(aliases_hash)
61
+ end
62
+
63
+ def create_instance_aliases(aliases_hash)
64
+ Alias.manager.create_aliases(:instance_method, aliases_hash)
65
+ end
66
+
67
+ def check_for_uncreated_aliases(lib, commands)
68
+ if (found_commands = Boson.commands.select {|e| commands.include?(e.name)}) && found_commands.find {|e| e.alias }
69
+ $stderr.puts "No aliases created for library #{lib.name} because it has no module"
70
+ end
71
+ end
72
+ end
73
+ include AliasLib
74
+ end
75
+ end
@@ -0,0 +1,90 @@
1
+ # Extracts arguments and their default values from methods either by
2
+ # by scraping a method's text or with method_added and brute force eval (thanks to
3
+ # {eigenclass}[http://eigenclass.org/hiki/method+arguments+via+introspection]).
4
+ module Boson::ArgumentInspector
5
+ extend self
6
+
7
+ # Max number of arguments extracted per method with scrape_with_eval
8
+ MAX_ARGS = 10
9
+ # Scrapes non-private methods for argument names and default values.
10
+ # Returns arguments as array of argument arrays with optional default value as a second element.
11
+ # ====Examples:
12
+ # def meth1(arg1, arg2='val', options={}) -> [['arg1'], ['arg2', 'val'], ['options', {}]]
13
+ # def meth2(*args) -> [['*args']]
14
+ def scrape_with_eval(meth, klass, object)
15
+ unless %w[initialize].include?(meth.to_s)
16
+ return if class << object; private_instance_methods(true).map {|e| e.to_s } end.include?(meth.to_s)
17
+ end
18
+ params, values, arity, num_args = trace_method_args(meth, klass, object)
19
+ return if local_variables == params # nothing new found
20
+ format_arguments(params, values, arity, num_args)
21
+ rescue Exception
22
+ print_debug_message(klass, meth) if Boson.debug
23
+ ensure
24
+ set_trace_func(nil)
25
+ end
26
+
27
+ def print_debug_message(klass, meth) #:nodoc:
28
+ warn "DEBUG: Error while scraping arguments from #{klass.to_s[/\w+$/]}##{meth}: #{$!.message}"
29
+ end
30
+
31
+ # process params + values to return array of argument arrays
32
+ def format_arguments(params, values, arity, num_args) #:nodoc:
33
+ params ||= []
34
+ params = params[0,num_args]
35
+ params.inject([[], 0]) do |(a, i), x|
36
+ if Array === values[i]
37
+ [a << ["*#{x}"], i+1]
38
+ else
39
+ if arity < 0 && i >= arity.abs - 1
40
+ [a << [x.to_s, values[i]], i + 1]
41
+ else
42
+ [a << [x.to_s], i+1]
43
+ end
44
+ end
45
+ end.first
46
+ end
47
+
48
+ def trace_method_args(meth, klass, object) #:nodoc:
49
+ file = line = params = values = nil
50
+ arity = klass.instance_method(meth).arity
51
+ set_trace_func lambda{|event, file, line, id, binding, classname|
52
+ begin
53
+ if event[/call/] && classname == klass && id == meth
54
+ params = eval("local_variables", binding)
55
+ values = eval("local_variables.map{|x| eval(x.to_s)}", binding)
56
+ throw :done
57
+ end
58
+ rescue Exception
59
+ print_debug_message(klass, meth) if Boson.debug
60
+ end
61
+ }
62
+ if arity >= 0
63
+ num_args = arity
64
+ catch(:done){ object.send(meth, *(0...arity)) }
65
+ else
66
+ num_args = 0
67
+ # determine number of args (including splat & block)
68
+ MAX_ARGS.downto(arity.abs - 1) do |i|
69
+ catch(:done) do
70
+ begin
71
+ object.send(meth, *(0...i))
72
+ rescue Exception
73
+ end
74
+ end
75
+ # all nils if there's no splat and we gave too many args
76
+ next if !values || values.compact.empty?
77
+ k = nil
78
+ values.each_with_index{|x,j| break (k = j) if Array === x}
79
+ num_args = k ? k+1 : i
80
+ break
81
+ end
82
+ args = (0...arity.abs-1).to_a
83
+ catch(:done) do
84
+ args.empty? ? object.send(meth) : object.send(meth, *args)
85
+ end
86
+ end
87
+ set_trace_func(nil)
88
+ return [params, values, arity, num_args]
89
+ end
90
+ end
@@ -0,0 +1,67 @@
1
+ module Boson::Commands::Core #:nodoc:
2
+ extend self
3
+
4
+ def config
5
+ command_attributes = Boson::Command::ATTRIBUTES + [:usage, :full_name, :render_options]
6
+ library_attributes = Boson::Library::ATTRIBUTES + [:library_type]
7
+
8
+ commands = {
9
+ 'usage'=>{:desc=>"Print a command's usage", :options=>{
10
+ :verbose=>{:desc=>"Display global options", :type=>:boolean},
11
+ :render_options=>{:desc=>"Render options for option tables", :default=>{},
12
+ :keys=>[:vertical, :fields, :hide_empty]} } },
13
+ 'commands'=>{
14
+ :desc=>"List or search commands. Query must come before any options.", :default_option=>'query',
15
+ :options=>{ :index=>{:type=>:boolean, :desc=>"Searches index"},
16
+ :local=>{:type=>:boolean, :desc=>"Local commands only" } },
17
+ :render_options=>{
18
+ [:headers,:H]=>{:default=>{:desc=>'description'}},
19
+ :query=>{:keys=>command_attributes, :default_keys=>'full_name'},
20
+ :fields=>{:default=>[:full_name, :lib, :alias, :usage, :desc], :values=>command_attributes, :enum=>false},
21
+ :filters=>{:default=>{:render_options=>:inspect, :options=>:inspect, :args=>:inspect, :config=>:inspect}}
22
+ }
23
+ },
24
+ 'libraries'=>{
25
+ :desc=>"List or search libraries. Query must come before any options.", :default_option=>'query',
26
+ :options=>{ :index=>{:type=>:boolean, :desc=>"Searches index"},
27
+ :local=>{:type=>:boolean, :desc=>"Local libraries only" } },
28
+ :render_options=>{
29
+ :query=>{:keys=>library_attributes, :default_keys=>'name'},
30
+ :fields=>{:default=>[:name, :commands, :gems, :library_type], :values=>library_attributes, :enum=>false},
31
+ :filters=>{:default=>{:gems=>[:join, ','],:commands=>:size}, :desc=>"Filters to apply to library fields" }}
32
+ },
33
+ 'load_library'=>{:desc=>"Load a library", :options=>{[:verbose,:V]=>true}}
34
+ }
35
+
36
+ {:namespace=>false, :library_file=>File.expand_path(__FILE__), :commands=>commands}
37
+ end
38
+
39
+ def commands(options={})
40
+ cmds = options[:index] ? (Boson::Index.read || true) && Boson::Index.commands : Boson.commands
41
+ options[:local] ? cmds.select {|e| e.library && e.library.local? } : cmds
42
+ end
43
+
44
+ def libraries(options={})
45
+ libs = options[:index] ? (Boson::Index.read || true) && Boson::Index.libraries : Boson.libraries
46
+ options[:local] ? libs.select {|e| e.local? } : libs
47
+ end
48
+
49
+ def load_library(library, options={})
50
+ Boson::Manager.load(library, options)
51
+ end
52
+
53
+ def usage(command, options={})
54
+ puts Boson::Command.usage(command)
55
+
56
+ if (cmd = Boson::Command.find(command))
57
+ if cmd.options && !cmd.options.empty?
58
+ puts "\nLOCAL OPTIONS"
59
+ cmd.option_parser.print_usage_table options[:render_options].dup.merge(:local=>true)
60
+ end
61
+ if options[:verbose] && cmd.render_option_parser
62
+ puts "\nGLOBAL OPTIONS"
63
+ cmd.render_option_parser.print_usage_table options[:render_options].dup
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,19 @@
1
+ module Boson::Commands::ViewCore
2
+ extend self
3
+
4
+ def config
5
+ commands = {
6
+ 'render'=>{:desc=>"Render any object using Hirb"},
7
+ 'menu'=>{:desc=>"Provide a menu to multi-select elements from a given array"}
8
+ }
9
+ {:namespace=>false, :library_file=>File.expand_path(__FILE__), :commands=>commands}
10
+ end
11
+
12
+ def render(object, options={})
13
+ Boson::View.render(object, options)
14
+ end
15
+
16
+ def menu(arr, options={}, &block)
17
+ Hirb::Console.format_output(arr, options.merge(:class=>"Hirb::Menu"), &block)
18
+ end
19
+ end
@@ -0,0 +1,153 @@
1
+ module Boson::Commands::WebCore
2
+ extend self
3
+
4
+ def config #:nodoc:
5
+ commands = {
6
+ 'get'=>{ :desc=>"Gets the body of a url", :args=>[['url'],['options', {}]]},
7
+ 'post'=>{ :desc=>'Posts to a url', :args=>[['url'],['options', {}]]},
8
+ 'build_url'=>{ :desc=>"Builds a url, escaping the given params", :args=>[['url'],['params']]},
9
+ 'browser'=>{ :desc=>"Opens urls in a browser on a Mac"},
10
+ 'install'=>{ :desc=>"Installs a library by url. Library should then be loaded with load_library.",
11
+ :args=>[['url'],['options', {}]],
12
+ :options=> { :name=>{:type=>:string, :desc=>"Library name to save to"},
13
+ :force=>{:type=>:boolean, :desc=>'Overwrites an existing library'},
14
+ :default=>{:type=>:boolean, :desc=>'Adds library as a default library to main config file'},
15
+ :module_wrap=>{:type=>:boolean, :desc=>"Wraps a module around install using library name"},
16
+ :method_wrap=>{:type=>:boolean, :desc=>"Wraps a method and module around installed library using library name"}}
17
+ }
18
+ }
19
+
20
+ {:library_file=>File.expand_path(__FILE__), :commands=>commands, :namespace=>false}
21
+ end
22
+
23
+ # Requires libraries only once before defining method with given block
24
+ def self.def_which_requires(meth, *libs, &block)
25
+ define_method(meth) do |*args|
26
+ libs.each {|e| require e }
27
+ define_method(meth, block).call(*args)
28
+ end
29
+ end
30
+
31
+ def_which_requires(:get, 'net/https') do |*args|
32
+ url, options = args[0], args[1] || {}
33
+ url = build_url(url, options[:params]) if options[:params]
34
+ Get.new(url).request(options)
35
+ end
36
+
37
+ def_which_requires(:build_url, 'cgi') do |url, params|
38
+ url + (url[/\?/] ? '&' : "?") + params.map {|k,v|
39
+ v = v.is_a?(Array) ? v.join(' ') : v.to_s
40
+ "#{k}=#{CGI.escape(v)}"
41
+ }.join("&")
42
+ end
43
+
44
+ def_which_requires(:post, 'uri', 'net/http') do |*args|
45
+ url, options = args[0], args[1] || {}
46
+ (res = Net::HTTP.post_form(URI.parse(url), options)) && res.body
47
+ end
48
+
49
+ def install(url, options={}) #:nodoc:
50
+ options[:name] ||= strip_name_from_url(url)
51
+ return puts("Please give a library name for this url.") if options[:name].empty?
52
+ filename = File.join ::Boson.repo.commands_dir, "#{options[:name]}.rb"
53
+ return puts("Library name #{options[:name]} already exists. Try a different name.") if File.exists?(filename) && !options[:force]
54
+
55
+ file_string = get(url) or raise "Unable to fetch url"
56
+ file_string = "# Originally from #{url}\n"+file_string
57
+ file_string = wrap_install(file_string, options) if options[:method_wrap] || options[:module_wrap]
58
+
59
+ File.open(filename, 'w') {|f| f.write file_string }
60
+ Boson.repo.update_config {|c| (c[:defaults] ||= []) << options[:name] } if options[:default]
61
+ puts "Saved to #{filename}."
62
+ end
63
+
64
+ # non-mac users should override this with the launchy gem
65
+ def browser(*urls)
66
+ system('open', *urls)
67
+ end
68
+
69
+ private
70
+ def wrap_install(file_string, options)
71
+ indent = " "
72
+ unless (mod_name = ::Boson::Util.camelize(options[:name]))
73
+ return puts("Can't wrap install with name #{options[:name]}")
74
+ end
75
+
76
+ file_string.gsub!(/(^)/,'\1'+indent)
77
+ file_string = "def #{options[:name]}\n#{file_string}\nend".gsub(/(^)/,'\1'+indent) if options[:method_wrap]
78
+ "module #{mod_name}\n#{file_string}\nend"
79
+ end
80
+
81
+ def strip_name_from_url(url)
82
+ url[/\/([^\/.]+)(\.[a-z]+)?$/, 1].to_s.gsub('-', '_').gsub(/[^a-zA-Z_]/, '')
83
+ end
84
+
85
+ # Used by the get command to make get requests and optionally parse json and yaml.
86
+ # Ruby 1.8.x is dependent on json gem for parsing json.
87
+ # See Get.request for options a request can take.
88
+ class Get
89
+ FORMAT_HEADERS = {
90
+ :json=>%w{application/json text/json application/javascript text/javascript},
91
+ :yaml=>%w{application/x-yaml text/yaml}
92
+ } #:nodoc:
93
+
94
+ def initialize(url, options={})
95
+ @url, @options = url, options
96
+ end
97
+
98
+ # Returns the response body string or a parsed data structure. Returns nil if request fails. By default expects response
99
+ # to be 200.
100
+ # ==== Options:
101
+ # [:any_response] Returns body string for any response code. Default is false.
102
+ # [:parse] Parse the body into either json or yaml. Expects a valid format or if true autodetects one.
103
+ # Default is false.
104
+ # [:raise_error] Raises any original errors when parsing or fetching url instead of handling errors silently.
105
+ def request(options={})
106
+ @options.merge! options
107
+ body = get_body
108
+ body && @options[:parse] ? parse_body(body) : body
109
+ end
110
+
111
+ private
112
+ # Returns body string if successful or nil if not.
113
+ def get_body
114
+ uri = URI.parse(@url)
115
+ @response = get_response(uri)
116
+ (@options[:any_response] || @response.code == '200') ? @response.body : nil
117
+ rescue
118
+ @options[:raise_error] ? raise : puts("Error: GET '#{@url}' -> #{$!.class}: #{$!.message}")
119
+ end
120
+
121
+ def get_response(uri)
122
+ net = Net::HTTP.new(uri.host, uri.port)
123
+ net.verify_mode = OpenSSL::SSL::VERIFY_NONE if uri.scheme == 'https'
124
+ net.use_ssl = true if uri.scheme == 'https'
125
+ net.start {|http| http.request_get(uri.request_uri) }
126
+ end
127
+
128
+ # Returns nil if dependencies or parsing fails
129
+ def parse_body(body)
130
+ format = determine_format(@options[:parse])
131
+ case format
132
+ when :json
133
+ unless ::Boson::Util.safe_require 'json'
134
+ return puts("Install the json gem to parse json: sudo gem install json")
135
+ end
136
+ JSON.parse body
137
+ when :yaml
138
+ YAML::load body
139
+ else
140
+ puts "Can't parse this format."
141
+ end
142
+ rescue
143
+ @options[:raise_error] ? raise : puts("Error while parsing #{format} response of '#{@url}': #{$!.class}")
144
+ end
145
+
146
+ def determine_format(format)
147
+ return format.to_sym if %w{json yaml}.include?(format.to_s)
148
+ return :json if FORMAT_HEADERS[:json].include?(@response.content_type)
149
+ return :yaml if FORMAT_HEADERS[:yaml].include?(@response.content_type)
150
+ nil
151
+ end
152
+ end
153
+ end