mithril 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. data/CHANGELOG.md +31 -0
  2. data/README.md +0 -0
  3. data/bin/mithril +5 -0
  4. data/lib/mithril.rb +13 -0
  5. data/lib/mithril/controllers.rb +7 -0
  6. data/lib/mithril/controllers/abstract_controller.rb +130 -0
  7. data/lib/mithril/controllers/mixins.rb +7 -0
  8. data/lib/mithril/controllers/mixins/actions_base.rb +114 -0
  9. data/lib/mithril/controllers/mixins/help_actions.rb +46 -0
  10. data/lib/mithril/controllers/mixins/mixin_with_actions.rb +27 -0
  11. data/lib/mithril/controllers/proxy_controller.rb +89 -0
  12. data/lib/mithril/mixin.rb +33 -0
  13. data/lib/mithril/parsers.rb +7 -0
  14. data/lib/mithril/parsers/simple_parser.rb +57 -0
  15. data/lib/mithril/request.rb +11 -0
  16. data/lib/mithril/version.rb +5 -0
  17. data/spec/matchers/be_kind_of_spec.rb +50 -0
  18. data/spec/matchers/construct_spec.rb +49 -0
  19. data/spec/matchers/respond_to_spec.rb +158 -0
  20. data/spec/mithril/controllers/_text_controller_helper.rb +81 -0
  21. data/spec/mithril/controllers/abstract_controller_helper.rb +118 -0
  22. data/spec/mithril/controllers/abstract_controller_spec.rb +15 -0
  23. data/spec/mithril/controllers/mixins/actions_base_helper.rb +121 -0
  24. data/spec/mithril/controllers/mixins/actions_base_spec.rb +18 -0
  25. data/spec/mithril/controllers/mixins/help_actions_helper.rb +111 -0
  26. data/spec/mithril/controllers/mixins/help_actions_spec.rb +19 -0
  27. data/spec/mithril/controllers/mixins/mixin_with_actions_spec.rb +44 -0
  28. data/spec/mithril/controllers/proxy_controller_helper.rb +111 -0
  29. data/spec/mithril/controllers/proxy_controller_spec.rb +14 -0
  30. data/spec/mithril/mixin_helper.rb +54 -0
  31. data/spec/mithril/mixin_spec.rb +17 -0
  32. data/spec/mithril/parsers/simple_parser_spec.rb +85 -0
  33. data/spec/mithril/request_spec.rb +72 -0
  34. data/spec/mithril_spec.rb +25 -0
  35. data/spec/spec_helper.rb +15 -0
  36. data/spec/support/factories/action_factory.rb +7 -0
  37. data/spec/support/factories/request_factory.rb +11 -0
  38. data/spec/support/matchers/be_kind_of.rb +23 -0
  39. data/spec/support/matchers/construct.rb +49 -0
  40. data/spec/support/matchers/respond_to.rb +52 -0
  41. metadata +142 -0
@@ -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
File without changes
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'mithril'
4
+
5
+ puts "Greetings, programs!"
@@ -0,0 +1,13 @@
1
+ # lib/mithril.rb
2
+
3
+ require 'logger'
4
+
5
+ module Mithril
6
+ def self.logger
7
+ return @logger ||= Logger.new(STDOUT)
8
+ end # class accessor logger
9
+
10
+ def self.logger=(logger)
11
+ @logger = logger
12
+ end # class mutator logger=
13
+ end # module
@@ -0,0 +1,7 @@
1
+ # lib/mithril/controllers.rb
2
+
3
+ require 'mithril'
4
+
5
+ module Mithril
6
+ module Controllers; end
7
+ end # module
@@ -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,7 @@
1
+ # lib/mithril/controllers/mixins.rb
2
+
3
+ require 'mithril/controllers'
4
+
5
+ module Mithril::Controllers
6
+ module Mixins; end
7
+ 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
+