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