mithril 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +31 -0
- data/README.md +0 -0
- data/bin/mithril +5 -0
- data/lib/mithril.rb +13 -0
- data/lib/mithril/controllers.rb +7 -0
- data/lib/mithril/controllers/abstract_controller.rb +130 -0
- data/lib/mithril/controllers/mixins.rb +7 -0
- data/lib/mithril/controllers/mixins/actions_base.rb +114 -0
- data/lib/mithril/controllers/mixins/help_actions.rb +46 -0
- data/lib/mithril/controllers/mixins/mixin_with_actions.rb +27 -0
- data/lib/mithril/controllers/proxy_controller.rb +89 -0
- data/lib/mithril/mixin.rb +33 -0
- data/lib/mithril/parsers.rb +7 -0
- data/lib/mithril/parsers/simple_parser.rb +57 -0
- data/lib/mithril/request.rb +11 -0
- data/lib/mithril/version.rb +5 -0
- data/spec/matchers/be_kind_of_spec.rb +50 -0
- data/spec/matchers/construct_spec.rb +49 -0
- data/spec/matchers/respond_to_spec.rb +158 -0
- data/spec/mithril/controllers/_text_controller_helper.rb +81 -0
- data/spec/mithril/controllers/abstract_controller_helper.rb +118 -0
- data/spec/mithril/controllers/abstract_controller_spec.rb +15 -0
- data/spec/mithril/controllers/mixins/actions_base_helper.rb +121 -0
- data/spec/mithril/controllers/mixins/actions_base_spec.rb +18 -0
- data/spec/mithril/controllers/mixins/help_actions_helper.rb +111 -0
- data/spec/mithril/controllers/mixins/help_actions_spec.rb +19 -0
- data/spec/mithril/controllers/mixins/mixin_with_actions_spec.rb +44 -0
- data/spec/mithril/controllers/proxy_controller_helper.rb +111 -0
- data/spec/mithril/controllers/proxy_controller_spec.rb +14 -0
- data/spec/mithril/mixin_helper.rb +54 -0
- data/spec/mithril/mixin_spec.rb +17 -0
- data/spec/mithril/parsers/simple_parser_spec.rb +85 -0
- data/spec/mithril/request_spec.rb +72 -0
- data/spec/mithril_spec.rb +25 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/support/factories/action_factory.rb +7 -0
- data/spec/support/factories/request_factory.rb +11 -0
- data/spec/support/matchers/be_kind_of.rb +23 -0
- data/spec/support/matchers/construct.rb +49 -0
- data/spec/support/matchers/respond_to.rb +52 -0
- metadata +142 -0
data/CHANGELOG.md
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
## Development Versions
|
2
|
+
|
3
|
+
#### Version 0.2.0
|
4
|
+
* Implemented ProxyController
|
5
|
+
|
6
|
+
#### Version 0.1.2
|
7
|
+
* Added HelpActions mixin
|
8
|
+
|
9
|
+
#### Version 0.1.1
|
10
|
+
* Added logger property to Mithril module
|
11
|
+
|
12
|
+
#### Version 0.1.0
|
13
|
+
* Implemented AbstractController
|
14
|
+
|
15
|
+
#### Version 0.0.4
|
16
|
+
* Renamed ActionMixin to MixinWithActions for clarity
|
17
|
+
* Improved specs for Mixin, MixinWithActions
|
18
|
+
|
19
|
+
#### Version 0.0.3
|
20
|
+
* Implemented Mithril::Parsers::SimpleParser
|
21
|
+
|
22
|
+
#### Version 0.0.2
|
23
|
+
* Implemented Mithril::Controllers::Mixins::ActionsBase
|
24
|
+
* Implemented Mithril::Request
|
25
|
+
* Added and monkey-patched RSpec Matchers
|
26
|
+
|
27
|
+
#### Version 0.0.1
|
28
|
+
* Implemented Mithril::Mixin
|
29
|
+
|
30
|
+
#### Version 0.0.0
|
31
|
+
* Smoke test
|
data/README.md
ADDED
File without changes
|
data/bin/mithril
ADDED
data/lib/mithril.rb
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
# lib/mithril/controllers/abstract_controller.rb
|
2
|
+
|
3
|
+
require 'mithril/controllers'
|
4
|
+
require 'mithril/controllers/mixins/actions_base'
|
5
|
+
require 'mithril/controllers/mixins/mixin_with_actions'
|
6
|
+
require 'mithril/parsers/simple_parser'
|
7
|
+
|
8
|
+
module Mithril::Controllers
|
9
|
+
# Base class for Mithril controllers. Extending controller functionality
|
10
|
+
# can be implemented either through direct class inheritance, e.g.
|
11
|
+
#
|
12
|
+
# ModuleController > ProxyController > AbstractController
|
13
|
+
#
|
14
|
+
# or through mixing in shared functionality with a Mixin, but all controllers
|
15
|
+
# ought to extend AbstractController unless you have a very compelling reason
|
16
|
+
# otherwise.
|
17
|
+
class AbstractController
|
18
|
+
extend Mithril::Controllers::Mixins::MixinWithActions
|
19
|
+
|
20
|
+
mixin Mithril::Controllers::Mixins::ActionsBase
|
21
|
+
|
22
|
+
# @param [Mithril::Request] request Request object containing the volatile
|
23
|
+
# state information needed to build the controller and execute commands.
|
24
|
+
# @raise [ArgumentError] If request does not respond to :session.
|
25
|
+
def initialize(request)
|
26
|
+
unless request.respond_to? :session
|
27
|
+
raise ArgumentError.new "expected request to respond_to :session"
|
28
|
+
end # unless
|
29
|
+
|
30
|
+
@request = request
|
31
|
+
end # constructor
|
32
|
+
|
33
|
+
def class_name
|
34
|
+
(self.class.name || "").split("::").last
|
35
|
+
end # accessor class_name
|
36
|
+
private :class_name
|
37
|
+
|
38
|
+
#########################
|
39
|
+
### Executing Actions ###
|
40
|
+
|
41
|
+
# The parser object used to process input into a command and an arguments
|
42
|
+
# object.
|
43
|
+
def parser
|
44
|
+
@parser ||= Mithril::Parsers::SimpleParser.new(self)
|
45
|
+
end # method parser
|
46
|
+
|
47
|
+
# Delegates to the parser instance. Note that if a custom parser is used
|
48
|
+
# that does not conform to the expected API, the return values may be
|
49
|
+
# different than listed.
|
50
|
+
# @param [Object] input The input to be parsed.
|
51
|
+
# @return [Array] The first element will be the matched command, or nil if
|
52
|
+
# no command was found. The second element will be the arguments object
|
53
|
+
# created by the parser.
|
54
|
+
def parse_command(input)
|
55
|
+
self.parser.parse_command input
|
56
|
+
end # method parse_command
|
57
|
+
|
58
|
+
# @return [Array] All commands available to this controller.
|
59
|
+
def commands
|
60
|
+
actions.keys.map do |key| key.to_s.gsub '_', ' '; end
|
61
|
+
end # method commands
|
62
|
+
|
63
|
+
# @param [String, Symbol] text The command to check. The parameter is
|
64
|
+
# converted to a String, and must exactly match a command available
|
65
|
+
# to the controller.
|
66
|
+
# @return [Boolean] True if this controller has the specified command.
|
67
|
+
# Otherwise false.
|
68
|
+
# @see #can_invoke?
|
69
|
+
def has_command?(text)
|
70
|
+
commands.include? text.to_s
|
71
|
+
end # method has_command?
|
72
|
+
|
73
|
+
# @example Using :has_command? and :can_invoke?
|
74
|
+
# # With an action "do" defined
|
75
|
+
# has_command?("do something") #=> false
|
76
|
+
# can_invoke?("do something") #=> true
|
77
|
+
# @param [Object] input The sample input to be parsed. Type and format will
|
78
|
+
# depend on the parser used.
|
79
|
+
# @return [Boolean] True if this controller has a command matching the
|
80
|
+
# provided input. Otherwise false.
|
81
|
+
# @see #has_command?
|
82
|
+
def can_invoke?(input)
|
83
|
+
self.allow_empty_action? || !self.parse_command(input).first.nil?
|
84
|
+
end # method can_invoke?
|
85
|
+
|
86
|
+
# Default output when a command cannot be found for a given input.
|
87
|
+
# @param [Object] input The input that failed to match a command.
|
88
|
+
# @return [Object]
|
89
|
+
def command_missing(input)
|
90
|
+
"I'm sorry, I don't know how to \"#{input.to_s}\". Please try another" +
|
91
|
+
" command, or enter \"help\" for assistance."
|
92
|
+
end # method command_missing
|
93
|
+
|
94
|
+
# Parses input into a command and arguments, then matches the command to an
|
95
|
+
# available action (if any), invokes the action, and returns the result. If
|
96
|
+
# there is no matching command, but the controller has an empty action :""
|
97
|
+
# defined and allow_empty_action? evaluates to true, the controller will
|
98
|
+
# instead invoke the empty action with the parsed arguments. If no matching
|
99
|
+
# command is found, returns the result of command_missing.
|
100
|
+
#
|
101
|
+
# @param [Object] input The input to be parsed and evaluated. Type and
|
102
|
+
# format will depend on the parser used.
|
103
|
+
# @return [Object] The result of the command. If no command is found,
|
104
|
+
# returns the result of command_missing.
|
105
|
+
# @see #allow_empty_action?
|
106
|
+
# @see #command_missing
|
107
|
+
# @see #parse_command
|
108
|
+
def invoke_command(text)
|
109
|
+
# Mithril.logger.debug "#{class_name}.invoke_command(), text =" +
|
110
|
+
# " #{text.inspect}"
|
111
|
+
|
112
|
+
command, args = self.parse_command text
|
113
|
+
|
114
|
+
if self.has_action? command
|
115
|
+
self.invoke_action command, args
|
116
|
+
elsif allow_empty_action?
|
117
|
+
self.invoke_action :"", args
|
118
|
+
else
|
119
|
+
command_missing(text)
|
120
|
+
end # unless-elsif
|
121
|
+
end # method invoke_command
|
122
|
+
|
123
|
+
# If this method evaluates to true, if the controller does not recognize an
|
124
|
+
# action from the input text, it will attempt to invoke the empty action
|
125
|
+
# :"" with the full arguments list.
|
126
|
+
def allow_empty_action?
|
127
|
+
false
|
128
|
+
end # method allow_empty_action?
|
129
|
+
end # class AbstractController
|
130
|
+
end # module
|
@@ -0,0 +1,114 @@
|
|
1
|
+
# lib/mithril/controllers/mixins/actions_base.rb
|
2
|
+
|
3
|
+
require 'mithril/controllers/mixins/mixin_with_actions'
|
4
|
+
|
5
|
+
module Mithril::Controllers::Mixins
|
6
|
+
# Core functions for implementing a command+args response model. ActionsBase
|
7
|
+
# should be mixed in to controllers, either directly or via an intermediate
|
8
|
+
# Mixin that implements default or shared actions.
|
9
|
+
#
|
10
|
+
# @see Mithril::Mixin
|
11
|
+
# @see Mithril::Controllers::Mixins::ActionsBase::ClassMethods
|
12
|
+
module ActionsBase
|
13
|
+
extend Mithril::Controllers::Mixins::MixinWithActions
|
14
|
+
|
15
|
+
# These methods get extended into the class of the controller through the
|
16
|
+
# magic of Mixin.
|
17
|
+
#
|
18
|
+
# @see Mithril::Mixin
|
19
|
+
# @see Mithril::Controllers::Mixins::ActionsBase
|
20
|
+
module ClassMethods
|
21
|
+
# Defines an action to which the controller will respond.
|
22
|
+
#
|
23
|
+
# @param [Symbol, String] key Best practice is to use snake_case,
|
24
|
+
# e.g. all lower-case letters, with words separated by underscores. It
|
25
|
+
# *ought* to work anyway, but caveat lector.
|
26
|
+
# @param [Hash] params Optional. Expects a hash of configuration values.
|
27
|
+
# @option params [Boolean] :private If set to true, creates a private
|
28
|
+
# action. Private actions are not listed by "help" and cannot be
|
29
|
+
# invoked directly by the user. They can be used to set up internal
|
30
|
+
# APIs.
|
31
|
+
# @yieldparam [Hash] session An object describing the current (volatile)
|
32
|
+
# state of the user session.
|
33
|
+
# @yieldparam [Object] arguments Additional information from the request
|
34
|
+
# to be passed into the action. Using the default parser, the arguments
|
35
|
+
# object will be an Array, but other parsers may pass in other data
|
36
|
+
# structures.
|
37
|
+
def define_action(key, params = {}, &block)
|
38
|
+
key = key.to_s.downcase.gsub(/\s+|\-+/,'_').intern
|
39
|
+
|
40
|
+
define_method :"action_#{key}", &block
|
41
|
+
|
42
|
+
@actions ||= {}
|
43
|
+
@actions[key] = params
|
44
|
+
end # class method define_action
|
45
|
+
|
46
|
+
# Lists the actions defined for the current controller by its base class.
|
47
|
+
# In almost all cases, the actions instance method should be used
|
48
|
+
# instead, as it handles class-based inheritance.
|
49
|
+
#
|
50
|
+
# @param [Boolean] allow_private If true, will include private actions.
|
51
|
+
# @return [Hash] The actions defined on the current controller class.
|
52
|
+
#
|
53
|
+
# @see Mithril::Controllers::Mixins::ActionsBase#actions
|
54
|
+
def actions(allow_private = false)
|
55
|
+
actions = @actions ||= {}
|
56
|
+
|
57
|
+
unless allow_private
|
58
|
+
actions = actions.select { |key, action| !action.has_key? :private }
|
59
|
+
end # unless
|
60
|
+
|
61
|
+
actions
|
62
|
+
end # class method actions
|
63
|
+
end # module ClassMethods
|
64
|
+
|
65
|
+
# @return [Mithril::Request]
|
66
|
+
attr_reader :request
|
67
|
+
|
68
|
+
# Lists the actions available to the current controller.
|
69
|
+
#
|
70
|
+
# @param [Boolean] allow_private If true, will include private actions.
|
71
|
+
# @return [Hash] The actions available to this controller.
|
72
|
+
def actions(allow_private = false)
|
73
|
+
actions = {}
|
74
|
+
|
75
|
+
actions.update(self.class.superclass.actions(allow_private)) if (klass = self.class.superclass).respond_to? :actions
|
76
|
+
|
77
|
+
actions.update(self.class.actions(allow_private))
|
78
|
+
|
79
|
+
actions
|
80
|
+
end # method actions
|
81
|
+
|
82
|
+
# @param [Symbol, String] key The action key to be checked.
|
83
|
+
# @param [Boolean] allow_private If true, will include private actions.
|
84
|
+
# @return [Boolean] True if the action is available on this controller with
|
85
|
+
# the specified private setting; false otherwise.
|
86
|
+
def has_action?(key, allow_private = false)
|
87
|
+
return false if key.nil?
|
88
|
+
|
89
|
+
self.actions(allow_private).has_key? key.intern
|
90
|
+
end # method has_action?
|
91
|
+
|
92
|
+
# Searches for a matching action. If found, calls the action with the given
|
93
|
+
# session hash and arguments list.
|
94
|
+
#
|
95
|
+
# @param [Symbol, String] command Converted to a string. The converted
|
96
|
+
# string must be an exact match (===) to the key passed in to
|
97
|
+
# klass.define_action.
|
98
|
+
# @param [Object] arguments Additional information from the request to be
|
99
|
+
# passed into the action. Using the default parser, the arguments object
|
100
|
+
# will be an Array, but other parsers may pass in other data structures.
|
101
|
+
# @param [Boolean] allow_private If true, can invoke private actions.
|
102
|
+
#
|
103
|
+
# @return [String, nil] The result of the action (should be a string), or
|
104
|
+
# nil if no action was invoked.
|
105
|
+
def invoke_action(command, arguments, allow_private = false)
|
106
|
+
session = request ? request.session || {} : {}
|
107
|
+
if self.has_action? command, allow_private
|
108
|
+
self.send :"action_#{command}", session, arguments
|
109
|
+
else
|
110
|
+
nil
|
111
|
+
end # if-else
|
112
|
+
end # method invoke_action
|
113
|
+
end # module ActionsBase
|
114
|
+
end # module
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# lib/mithril/controllers/mixins/help_actions.rb
|
2
|
+
|
3
|
+
require 'mithril/controllers/mixins/actions_base'
|
4
|
+
require 'mithril/controllers/mixins/mixin_with_actions'
|
5
|
+
|
6
|
+
module Mithril::Controllers::Mixins
|
7
|
+
module HelpActions
|
8
|
+
extend MixinWithActions
|
9
|
+
|
10
|
+
mixin ActionsBase
|
11
|
+
|
12
|
+
def help_message
|
13
|
+
""
|
14
|
+
end # method help_message
|
15
|
+
|
16
|
+
define_action :help do |session, arguments|
|
17
|
+
if arguments.first =~ /help/i
|
18
|
+
return "The help command provides general assistance, or information" +
|
19
|
+
" on specific commands.\n\nFormat: help COMMAND"
|
20
|
+
end # if
|
21
|
+
|
22
|
+
words = arguments.dup
|
23
|
+
key = nil
|
24
|
+
|
25
|
+
while 0 < words.count
|
26
|
+
cmd = words.join(' ')
|
27
|
+
key = words.join('_').intern
|
28
|
+
|
29
|
+
if self.respond_to?(:has_command?) && self.has_command?(cmd)
|
30
|
+
return self.invoke_command "#{cmd} help"
|
31
|
+
elsif self.has_action? key
|
32
|
+
return self.invoke_action key, %w(help)
|
33
|
+
end # if
|
34
|
+
|
35
|
+
words.pop
|
36
|
+
end # while
|
37
|
+
|
38
|
+
str = 0 < self.help_message.length ? "#{self.help_message}\n\n" : ""
|
39
|
+
|
40
|
+
names = self.respond_to?(:commands) ?
|
41
|
+
self.commands :
|
42
|
+
self.actions.map { |key, value| key.to_s.gsub('_',' ') }
|
43
|
+
str += "The following commands are available: #{names.uniq.join(", ")}"
|
44
|
+
end # action help
|
45
|
+
end # module
|
46
|
+
end # module
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# lib/mithril/controllers/mixins/mixin_with_actions.rb
|
2
|
+
|
3
|
+
require 'mithril/controllers/mixins'
|
4
|
+
require 'mithril/mixin'
|
5
|
+
|
6
|
+
module Mithril::Controllers::Mixins
|
7
|
+
module MixinWithActions
|
8
|
+
include Mithril::Mixin
|
9
|
+
|
10
|
+
private
|
11
|
+
# Extends the mixin method to implement inheritance of @actions ivar.
|
12
|
+
def mixin(source_module) # :doc:
|
13
|
+
super
|
14
|
+
|
15
|
+
self.mixins.each do |mixin|
|
16
|
+
next unless source_module.respond_to? :actions
|
17
|
+
if self.instance_variable_defined? :@actions
|
18
|
+
source_module.actions.each do |key, value|
|
19
|
+
@actions[key] = value
|
20
|
+
end # each
|
21
|
+
else
|
22
|
+
@actions = source_module.actions.dup
|
23
|
+
end # if-else
|
24
|
+
end # each
|
25
|
+
end # method mixin
|
26
|
+
end # module
|
27
|
+
end # module
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# lib/mithril/controllers/proxy_controller.rb
|
2
|
+
|
3
|
+
require 'mithril/controllers/abstract_controller'
|
4
|
+
|
5
|
+
module Mithril::Controllers
|
6
|
+
# Redirects incoming commands to a proxy controller based on the :proxy
|
7
|
+
# method. If no proxy is present, evaluates commands as normal.
|
8
|
+
class ProxyController < AbstractController
|
9
|
+
# The subject controller to which commands are redirected. Must be
|
10
|
+
# overriden in subclasses.
|
11
|
+
#
|
12
|
+
# @return [AbstractController]
|
13
|
+
def proxy
|
14
|
+
nil # override this in sub-classes
|
15
|
+
end # method proxy
|
16
|
+
|
17
|
+
# If evalutes to true, then any actions defined on this controller will be
|
18
|
+
# available even when a proxy is present. Defaults to true, but can be
|
19
|
+
# overriden in subclasses.
|
20
|
+
#
|
21
|
+
# @return [Boolean]
|
22
|
+
def allow_own_actions_while_proxied?
|
23
|
+
true
|
24
|
+
end # method allow_own_actions_while_proxied?
|
25
|
+
|
26
|
+
# @see AbstractController#commands
|
27
|
+
def commands
|
28
|
+
if proxy.nil?
|
29
|
+
super
|
30
|
+
elsif self.allow_own_actions_while_proxied?
|
31
|
+
super + proxy.commands
|
32
|
+
else
|
33
|
+
proxy.commands
|
34
|
+
end # if-elsif-else
|
35
|
+
end # method commands
|
36
|
+
|
37
|
+
# @see AbstractController#can_invoke?
|
38
|
+
alias_method :can_invoke_on_self?, :can_invoke?
|
39
|
+
|
40
|
+
# As can_invoke?, but returns true iff the command is available on this
|
41
|
+
# controller directly, as opposed to through a proxy subject.
|
42
|
+
#
|
43
|
+
# @param [String]
|
44
|
+
# @return [Boolean]
|
45
|
+
# @see ProxyController#can_invoke_on_self?
|
46
|
+
def can_invoke?(input)
|
47
|
+
if self.proxy.nil?
|
48
|
+
super
|
49
|
+
elsif self.allow_own_actions_while_proxied? && self.can_invoke_on_self?(input)
|
50
|
+
super
|
51
|
+
else
|
52
|
+
proxy.can_invoke?(input)
|
53
|
+
end # if-elsif-else
|
54
|
+
end # method can_invoke_on_self?
|
55
|
+
|
56
|
+
# If no proxy is present, attempts to invoke the command on self. If a
|
57
|
+
# proxy subject is present and the parent can invoke that command and
|
58
|
+
# allow_own_actions_while_proxied? evaluates to true, attempts to invoke
|
59
|
+
# the command on self. Otherwise, if the proxy subject can invoke that
|
60
|
+
# command, invokes the command on the proxy subject.
|
61
|
+
#
|
62
|
+
# This precedence order was selected to allow reflection within commands,
|
63
|
+
# e.g. the help action in Mixins::HelpActions that lists all available
|
64
|
+
# commands.
|
65
|
+
#
|
66
|
+
# @param [Object] input The input to be parsed and evaluated. Type and
|
67
|
+
# format will depend on the parser used.
|
68
|
+
# @return [Object] The result of the command. If no command is found,
|
69
|
+
# returns the result of command_missing.
|
70
|
+
# @see #proxy
|
71
|
+
# @see AbstractController#invoke_command
|
72
|
+
def invoke_command(input)
|
73
|
+
# Mithril.logger.debug "#{class_name}.invoke_command(), text =" +
|
74
|
+
# " #{text.inspect}, session = #{request.session.inspect}, proxy =" +
|
75
|
+
# " #{proxy}"
|
76
|
+
|
77
|
+
if self.proxy.nil?
|
78
|
+
super
|
79
|
+
elsif self.allow_own_actions_while_proxied? && self.can_invoke_on_self?(input)
|
80
|
+
super
|
81
|
+
elsif proxy.can_invoke? input
|
82
|
+
proxy.invoke_command input
|
83
|
+
else
|
84
|
+
command_missing(input)
|
85
|
+
end # if-elsif-else
|
86
|
+
end # method invoke_command
|
87
|
+
end # class ProxyController
|
88
|
+
end # module
|
89
|
+
|