mithril 0.2.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.
- 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
|
+
|