boson-more 0.1.0

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