mvcli 0.0.16 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/Changelog.md +4 -0
  3. data/Gemfile +2 -2
  4. data/README.md +6 -2
  5. data/app.rb +7 -0
  6. data/app/routes.rb +0 -0
  7. data/bin/mvcli +5 -0
  8. data/lib/mvcli.rb +1 -0
  9. data/lib/mvcli/action.rb +31 -0
  10. data/lib/mvcli/app.rb +23 -28
  11. data/lib/mvcli/controller.rb +20 -2
  12. data/lib/mvcli/core.rb +101 -0
  13. data/lib/mvcli/cortex.rb +38 -0
  14. data/lib/mvcli/form.rb +13 -0
  15. data/lib/mvcli/loader.rb +45 -1
  16. data/lib/mvcli/middleware.rb +13 -15
  17. data/lib/mvcli/path.rb +41 -0
  18. data/lib/mvcli/plugins.rb +19 -0
  19. data/lib/mvcli/plugins/controllers/plugins_controller.rb +23 -0
  20. data/lib/mvcli/plugins/forms/plugins/install_form.rb +6 -0
  21. data/lib/mvcli/plugins/models/plugins/installation_model.rb +33 -0
  22. data/lib/mvcli/plugins/providers/bundle_provider.rb +116 -0
  23. data/lib/mvcli/plugins/routes.rb +3 -0
  24. data/lib/mvcli/plugins/templates/plugins/index.txt.erb +3 -0
  25. data/lib/mvcli/plugins/templates/plugins/install.txt.erb +1 -0
  26. data/lib/mvcli/plugins/templates/plugins/uninstall.txt.erb +1 -0
  27. data/lib/mvcli/provisioning.rb +48 -38
  28. data/lib/mvcli/router.rb +34 -20
  29. data/lib/mvcli/std/extensions/erb_extension.rb +24 -0
  30. data/lib/mvcli/std/providers/argv_provider.rb +9 -0
  31. data/lib/mvcli/std/providers/config_provider.rb +29 -0
  32. data/lib/mvcli/std/providers/middleware_provider.rb +12 -0
  33. data/lib/mvcli/std/providers/router_provider.rb +17 -0
  34. data/lib/mvcli/std/routes.rb +2 -0
  35. data/lib/mvcli/version.rb +1 -1
  36. data/spec/features/managing_plugins_spec.rb +29 -0
  37. data/spec/fixtures/apps/trivium/app.rb +10 -0
  38. data/spec/fixtures/apps/trivium/app/routes.rb +0 -0
  39. data/spec/fixtures/apps/trivium/bin/trivium +6 -0
  40. data/spec/fixtures/apps/trivium/lib/trivium/version.rb +3 -0
  41. data/spec/fixtures/apps/trivium/trivium.gemspec +8 -0
  42. data/spec/fixtures/bin/trivium +3 -0
  43. data/spec/fixtures/plugins/timing-plugin/app/routes.rb +1 -0
  44. data/spec/fixtures/plugins/timing-plugin/lib/trivium-timing.rb +5 -0
  45. data/spec/fixtures/plugins/timing-plugin/trivium-timing.gemspec +9 -0
  46. data/spec/mvcli/action_spec.rb +31 -0
  47. data/spec/mvcli/controller_spec.rb +35 -0
  48. data/spec/mvcli/core_spec.rb +77 -0
  49. data/spec/mvcli/cortex_spec.rb +50 -0
  50. data/spec/mvcli/erb_spec.rb +1 -1
  51. data/spec/mvcli/form_spec.rb +1 -1
  52. data/spec/mvcli/loader_spec.rb +58 -13
  53. data/spec/mvcli/middleware/exception_logger_spec.rb +3 -3
  54. data/spec/mvcli/middleware/exit_status_spec.rb +10 -10
  55. data/spec/mvcli/middleware_spec.rb +28 -45
  56. data/spec/mvcli/path_spec.rb +13 -0
  57. data/spec/mvcli/path_spec/does/exist +1 -0
  58. data/spec/mvcli/plugins/providers/bundle_provider_spec.rb +23 -0
  59. data/spec/mvcli/provisioning_spec.rb +39 -37
  60. data/spec/mvcli/router_spec.rb +26 -32
  61. data/spec/mvcli/std/extensions/erb_extension_spec.rb +34 -0
  62. data/spec/mvcli/std/providers/middleware_provider_spec.rb +10 -0
  63. data/spec/mvcli/validatable_spec.rb +12 -12
  64. data/spec/spec_helper.rb +13 -1
  65. data/spec/support/aruba_helper.rb +76 -0
  66. metadata +69 -33
  67. data/lib/mvcli/actions.rb +0 -37
  68. data/lib/mvcli/renderer.rb +0 -16
  69. data/spec/mvcli/actions_spec.rb +0 -34
  70. data/spec/mvcli/dummy/app/providers/test_provider.rb +0 -5
@@ -0,0 +1,41 @@
1
+ require "pathname"
2
+
3
+ module MVCLI
4
+ class Path
5
+ def initialize(base)
6
+ @base = Pathname(base.to_s)
7
+ end
8
+
9
+ def exists?(path)
10
+ @base.join(path).exist?
11
+ end
12
+
13
+ def read(path)
14
+ @base.join(path).read
15
+ end
16
+
17
+ def join(path)
18
+ self.class.new @base.join path
19
+ end
20
+
21
+ def nearest(pattern)
22
+ ancestors.each do |dir|
23
+ if entry = dir.entries.find { |e| e.to_s.match pattern }
24
+ return dir.join entry
25
+ end
26
+ end
27
+ end
28
+
29
+ def ancestors(dir = @base)
30
+ if dir == dir.parent
31
+ []
32
+ else
33
+ [dir] + ancestors(dir.parent)
34
+ end
35
+ end
36
+
37
+ def to_s(path = nil)
38
+ path.nil? ? @base.to_s : @base.join(path).to_s
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,19 @@
1
+ require "mvcli/core"
2
+
3
+ module MVCLI
4
+ class Plugins < MVCLI::Core
5
+ requires :bundle, :cortex
6
+ self.path = File.expand_path '../plugins', __FILE__
7
+ self.namespace = ::MVCLI
8
+ self.identifier = 'mvcli-plugins'
9
+
10
+ def activate!
11
+ bundle.activate!
12
+ Core.drain do |cls|
13
+ core = cls.new if cls.path
14
+ core.activate!
15
+ cortex << core
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,23 @@
1
+ class MVCLI::PluginsController < MVCLI::Controller
2
+ requires :cortex, :argv, :bundle
3
+
4
+ def install
5
+ @installation = bundle.replace params[:name], form.attributes
6
+ respond_with @installation
7
+ end
8
+
9
+ def uninstall
10
+ @installation = bundle.remove params[:name]
11
+ respond_with @installation
12
+ end
13
+
14
+ def index
15
+ @plugins = bundle.plugins
16
+ respond_with @plugins
17
+ end
18
+
19
+ def respond_with(response, options = {})
20
+ response
21
+ end
22
+
23
+ end
@@ -0,0 +1,6 @@
1
+ class MVCLI::Plugins::InstallForm < MVCLI::Form
2
+
3
+ input :path, Pathname, decode: ->(s) { Pathname s }
4
+
5
+ validates(:path, "no such directory") { |path| path.exist? }
6
+ end
@@ -0,0 +1,33 @@
1
+ class MVCLI::Plugins::InstallationModel
2
+ requires :config
3
+
4
+ def initialize(form)
5
+ @form = form
6
+ end
7
+
8
+ def name
9
+ gemspec.name
10
+ end
11
+
12
+ def version
13
+ gemspec.version
14
+ end
15
+
16
+ def location
17
+ @form.path
18
+ end
19
+
20
+ def gemspec
21
+ gemspec = Gem::Specification.load Dir[@form.path.join('*.gemspec')].first
22
+ config.directory "plugins" do |dir|
23
+ target = dir.join(gemspec.name)
24
+ FileUtils.rm_rf target
25
+ target.make_symlink location
26
+ Gem.paths.path.unshift dir.to_s
27
+ request = Gem::RequestSet.new *gemspec.dependencies
28
+ request.resolve
29
+ request.install_into dir
30
+ end
31
+ return gemspec
32
+ end
33
+ end
@@ -0,0 +1,116 @@
1
+ class MVCLI::BundleProvider
2
+ requires :config
3
+
4
+ def value
5
+ self
6
+ end
7
+
8
+ def activate!
9
+ require activatefile if activatefile.exist?
10
+ end
11
+
12
+ def replace(name, options = {})
13
+ require 'bundler'
14
+ builder.dependencies.reject! { |dep| dep.name == name }
15
+ dep, *rest = builder.gem name, options
16
+ update! name
17
+ write!
18
+ return dep
19
+ end
20
+
21
+ def remove(gem_name)
22
+ require 'bundler'
23
+ dep = builder.dependencies.find { |d| d.name == gem_name }
24
+ fail "#{gem_name} is not an installed plugin" unless dep
25
+ builder.dependencies.reject! { |d| d == dep }
26
+ update! gem_name
27
+ write!
28
+ return dep
29
+ end
30
+
31
+ def plugins
32
+ lock.specs.select do |spec|
33
+ builder.dependencies.detect { |dep| dep.name == spec.name }
34
+ end
35
+ end
36
+
37
+ def gemfile
38
+ dir.join('Gemfile').tap do |path|
39
+ unless path.exist?
40
+ path.open "wb" do |file|
41
+ file.puts "source 'https://rubygems.org'"
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ def lockfile
48
+ dir.join 'Gemfile.lock'
49
+ end
50
+
51
+ def setupfile
52
+ dir.join 'bundle/bundler/setup.rb'
53
+ end
54
+
55
+ def activatefile
56
+ dir.join 'activate.rb'
57
+ end
58
+
59
+ def dir
60
+ config.directory('plugins')
61
+ end
62
+
63
+ def builder
64
+ @builder ||= Bundler::Dsl.new.tap do |builder|
65
+ builder.eval_gemfile gemfile
66
+ end
67
+ end
68
+
69
+ def lock
70
+ require 'bundler'
71
+ Bundler::LockfileParser.new lockfile.read
72
+ end
73
+
74
+ def write!
75
+ gemfile.open "w" do |file|
76
+ file.puts "source 'https://rubygems.org'"
77
+ builder.dependencies.each do |dep|
78
+ file << %|gem "#{dep.name}"|
79
+ if req = dep.requirements_list.first
80
+ file << %|, "#{req}"|
81
+ end
82
+ options = dep.source ? dep.source.options || {} : {}
83
+ options = Hash[options.map { |k, v| [k,v.to_s]}]
84
+ options.merge! "require" => dep.autorequire if dep.autorequire
85
+ file << %|, #{options.inspect}|
86
+ file << "\n"
87
+ end
88
+ end
89
+ activatefile.open "w" do |file|
90
+ file.puts "require #{setupfile.to_s.inspect}"
91
+ builder.dependencies.each do |dep|
92
+ require_names = dep.autorequire || [dep.name]
93
+ require_names.each do |name|
94
+ file.puts "require #{name.inspect}"
95
+ end
96
+ end
97
+ end
98
+ end
99
+
100
+ def update!(name)
101
+ path = Bundler.settings[:path]
102
+ original_definition_method = Bundler.method(:definition)
103
+ Bundler.with_clean_env do
104
+ ENV['BUNDLE_GEMFILE'] = gemfile.to_s
105
+ definition = builder.to_definition(lockfile, name => true)
106
+ Bundler.settings[:path] = dir.join('bundle').to_s
107
+ Dir.chdir dir do
108
+ Bundler.define_singleton_method(:definition) { definition }
109
+ Bundler::Installer.install dir, definition, standalone: [], "update" => true
110
+ end
111
+ end
112
+ ensure
113
+ Bundler.define_singleton_method(:definition, &original_definition_method)
114
+ Bundler.settings[:path] = path
115
+ end
116
+ end
@@ -0,0 +1,3 @@
1
+ match 'show plugins' => 'plugins#index'
2
+ match 'install plugin :name' => 'plugins#install'
3
+ match 'uninstall plugin :name' => 'plugins#uninstall'
@@ -0,0 +1,3 @@
1
+ <% for plugin in this %>
2
+ <%= plugin.name %> <%= plugin.version %>
3
+ <% end %>
@@ -0,0 +1 @@
1
+ Installed plugin <%= this.name %> <%= this.requirement %>
@@ -0,0 +1 @@
1
+ Uninstalled plugin <%= this.name %>
@@ -1,15 +1,14 @@
1
1
  require "map"
2
- require "active_support/concern"
3
- require "active_support/dependencies"
4
- require "mvcli/loader"
5
2
 
6
3
  module MVCLI
7
4
  module Provisioning
8
- extend ActiveSupport::Concern
9
- UnsatisfiedRequirement = Class.new StandardError
10
5
  MissingScope = Class.new StandardError
11
6
 
12
- module ClassMethods
7
+ def self.included(base)
8
+ base.send :extend, Requires
9
+ end
10
+
11
+ module Requires
13
12
  def requires(*deps)
14
13
  deps.each do |dep|
15
14
  self.send(:define_method, dep) {Scope[dep]}
@@ -17,61 +16,72 @@ module MVCLI
17
16
  end
18
17
  end
19
18
 
19
+
20
+ ::Object.send :include, self
21
+
20
22
  class Scope
21
- def initialize(command, provisioner)
22
- @command = command
23
- @provisioner = provisioner
23
+ requires :cortex
24
+
25
+ def initialize(options = {}, &block)
26
+ @providers = Map options
27
+ evaluate &block if block_given?
24
28
  end
25
29
 
26
30
  def [](name)
27
- name.to_s == "command" ? @command : @provisioner[name]
31
+ unless provider = @providers[name]
32
+ provider = @providers[name] = cortex.read :provider, name
33
+ end
34
+ if provider.respond_to?(:value)
35
+ provider.value
36
+ elsif provider.respond_to?(:instance_methods) && provider.instance_methods.member?(:value)
37
+ provider.new.value
38
+ else
39
+ provider
40
+ end
28
41
  end
29
42
 
30
- def evaluate
43
+ def evaluate(names = {})
31
44
  old = self.class.current
45
+ providers = @providers
46
+ @providers = Map @providers.to_hash.merge(names)
32
47
  self.class.current = self
33
48
  yield
34
49
  ensure
50
+ @providers = providers
35
51
  self.class.current = old
36
52
  end
37
53
 
38
- def self.current
39
- Thread.current[self.class.name]
40
- end
54
+ private
41
55
 
42
- def self.current!
43
- current or fail MissingScope, "attempting to access scope, but none is active!"
56
+ def const(value)
57
+ Constant.new value
44
58
  end
45
59
 
46
- def self.current=(scope)
47
- Thread.current[self.class.name] = scope
48
- end
60
+ class << self
61
+ def current
62
+ Thread.current[self.class.name]
63
+ end
49
64
 
50
- def self.[](name)
51
- current![name] or fail UnsatisfiedRequirement, "'#{name}' is required, but can't find it"
52
- end
53
- end
65
+ def current!
66
+ current or fail MissingScope, "attempting to access scope, but none is active!"
67
+ end
54
68
 
55
- class Provisioner
56
- def initialize
57
- @loader = Loader.new
58
- @providers = Map.new
59
- end
60
- def [](name)
61
- unless provider = @providers[name]
62
- provider = @providers[name] = @loader.load :provider, name
69
+ def current=(scope)
70
+ Thread.current[self.class.name] = scope
71
+ end
72
+
73
+ def [](name)
74
+ current![name]
63
75
  end
64
- provider.value
65
76
  end
66
- end
67
77
 
68
- class Middleware
69
- def call(command)
70
- Scope.new(command, Provisioner.new).evaluate do
71
- yield command
78
+ class Constant
79
+ attr_reader :value
80
+
81
+ def initialize(value)
82
+ @value = value
72
83
  end
73
84
  end
74
85
  end
75
- ::Object.send :include, self
76
86
  end
77
87
  end
@@ -5,35 +5,52 @@ require "mvcli/argv"
5
5
  module MVCLI
6
6
  class Router
7
7
  class RoutingError < StandardError; end
8
+ requires :actions
8
9
 
9
- def initialize(actions = nil)
10
- @actions = actions || Map.new
10
+ attr_reader :routes
11
+ attr_reader :macros
12
+
13
+ def initialize
11
14
  @routes = []
12
15
  @macros = []
13
16
  end
14
17
 
15
- def macro(options)
16
- @macros.push Macro.new options
17
- end
18
-
19
- def match(options)
20
- pattern, action = options.first
21
- options.delete pattern
22
- @routes << Route.new(pattern, @actions, action, options)
23
- end
24
-
25
18
  def call(command)
26
19
  argv = @macros.reduce(command.argv) do |args, macro|
27
20
  macro.expand args
28
21
  end
29
22
  @routes.each do |route|
30
- if match = route.match(argv)
31
- return match.call command
23
+ if action = route.match(argv)
24
+ return action
32
25
  end
33
26
  end
34
27
  fail RoutingError, "no route matches '#{command.argv.join ' '}'"
35
28
  end
36
29
 
30
+ alias_method :[], :call
31
+
32
+ class DSL < BasicObject
33
+ attr_reader :router
34
+
35
+ def initialize
36
+ @router = Router.new
37
+ end
38
+
39
+ def macro(options)
40
+ @router.macros.push Macro.new options
41
+ end
42
+
43
+ def match(options)
44
+ pattern, action = options.first
45
+ options.delete pattern
46
+ @router.routes << Route.new(router, pattern, action, options)
47
+ end
48
+
49
+ def proc(&body)
50
+ ::Proc.new &body
51
+ end
52
+ end
53
+
37
54
  class Macro
38
55
  def initialize(options)
39
56
  @pattern, @expansion = options.first
@@ -45,19 +62,16 @@ module MVCLI
45
62
  end
46
63
 
47
64
  class Route
48
- def initialize(pattern, actions, action, options = {})
65
+ def initialize(router, pattern, action, options = {})
49
66
  @pattern = Pattern.new pattern.to_s
50
- @actions, @action, @options = actions, action, options
67
+ @router, @action, @options = router, action, options
51
68
  end
52
69
 
53
70
  def match(argv)
54
71
  argv = MVCLI::Argv.new argv
55
72
  match = @pattern.match(argv.arguments)
56
73
  if match.matches?
57
- proc do |command|
58
- action = @actions[@action] or fail "no action found for #{@action}"
59
- action.call command, match.bindings
60
- end
74
+ @router.actions.new match, @action
61
75
  end
62
76
  end
63
77
  end