mvcli 0.0.16 → 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 (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