hypercuke 0.4.1

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.
@@ -0,0 +1,76 @@
1
+ module Hypercuke
2
+ class CLI
3
+
4
+ # I extract relevant information from a 'hcu' command line
5
+ class Parser
6
+ attr_reader :options
7
+ def initialize(hcu_command)
8
+ @tokens = tokenize(hcu_command)
9
+ parse_options
10
+ end
11
+
12
+ def layer_name
13
+ options[:layer_name]
14
+ end
15
+
16
+ private
17
+ attr_reader :tokens
18
+
19
+ def parse_options
20
+ @options = Hash.new
21
+
22
+ ignore_hcu
23
+ set_layer_name
24
+ set_mode_if_present
25
+ set_profile_if_present
26
+ set_other_args
27
+
28
+ options
29
+ end
30
+
31
+ def tokenize(input)
32
+ args =
33
+ case input
34
+ when Array ; input
35
+ when String ; input.split(/\s+/) # That's 4 years of CS education right there, baby
36
+ else fail "Don't know how to parse #{input.inspect}"
37
+ end
38
+ args.compact
39
+ end
40
+
41
+ # We might get the 'hcu' command name itself; just drop it on the floor
42
+ def ignore_hcu
43
+ tokens.shift if tokens.first =~ /\bhcu$/i
44
+ end
45
+
46
+ # This is the only required argument.
47
+ # TODO: Validate this against the list of known layers?
48
+ # ^ Would require loading local app's hypercuke config.
49
+ # ^ Would require allowing local app to *have* hypercuke config.
50
+ def set_layer_name
51
+ fail "Layer name is required" if tokens.empty?
52
+ options[:layer_name] = tokens.shift
53
+ end
54
+
55
+ def set_mode_if_present
56
+ unless tokens.first =~ /^-/
57
+ options[:mode] = tokens.shift
58
+ end
59
+ end
60
+
61
+ def set_profile_if_present
62
+ if profile_index = ( tokens.index('--profile') || tokens.index('-p') )
63
+ tokens.delete_at(profile_index) # don't care
64
+ options[:profile] = tokens.delete_at(profile_index)
65
+ end
66
+ end
67
+
68
+ def set_other_args
69
+ options[:other_args] = Array.new.tap do |rest|
70
+ rest << tokens.shift until tokens.empty?
71
+ end
72
+ end
73
+ end
74
+
75
+ end
76
+ end
@@ -0,0 +1,20 @@
1
+ module Hypercuke
2
+
3
+ class Config
4
+ attr_reader :layers, :topics
5
+ def initialize
6
+ @layers = NameList.new
7
+ @topics = NameList.new
8
+ end
9
+
10
+ def layer_names
11
+ layers.to_a
12
+ end
13
+
14
+ def topic_names
15
+ topics.to_a
16
+ end
17
+
18
+ end
19
+
20
+ end
@@ -0,0 +1,36 @@
1
+ require 'forwardable'
2
+
3
+ module Hypercuke
4
+ # I provide a way of passing state around between tests
5
+ # that isn't instance variables.
6
+ #
7
+ # This is handy even in plain old Cucumber-land (where if you typo an
8
+ # instance variable, you just get nil), and essential in a set of
9
+ # mostly-independent step adapter objects, each with their own private
10
+ # state.
11
+ class Context
12
+ def initialize
13
+ @hash = {}
14
+ end
15
+
16
+ extend Forwardable
17
+ # I support:
18
+ # - Hash-style getting and setting via square brackets,
19
+ # - fetch (as a pass-through to Hash),
20
+ def_delegators :@hash, *[
21
+ :[],
22
+ :[]=,
23
+ :fetch,
24
+ ]
25
+
26
+ # - And a variant of fetch that, if the key is not found, sets it
27
+ # for the next caller.
28
+ #
29
+ # This behavior is in the spirit of the ||= operator, except that
30
+ # it won't short-circuit and call the default value if the key is
31
+ # present, but set to nil or false.
32
+ def fetch_or_default(key, &block)
33
+ @hash[key] = @hash.fetch(key, &block)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,15 @@
1
+ # NOTE: this file should not be required from the Hypercuke gem itself.
2
+ # It's intended to be required from within the Cucumber environment
3
+ # (e.g., in features/support/env.rb or equivalent).
4
+
5
+ module Hypercuke
6
+ module CucumberIntegration
7
+ module WorldMixin
8
+ def step_driver
9
+ @step_driver ||= Hypercuke::StepDriver.new
10
+ end
11
+ end
12
+ end
13
+ end
14
+
15
+ World( Hypercuke::CucumberIntegration::WorldMixin )
@@ -0,0 +1,42 @@
1
+ module Hypercuke
2
+ module Error
3
+ def self.included(receiver)
4
+ receiver.extend ClassMethods
5
+ end
6
+
7
+ module ClassMethods
8
+ def wrap(exception)
9
+ message = translate_message(exception.message)
10
+ new(message).tap do |wrapping_exception|
11
+ wrapping_exception.set_backtrace exception.backtrace
12
+ end
13
+ end
14
+
15
+ def translate_message(message)
16
+ message # just here to be overridden
17
+ end
18
+ end
19
+ end
20
+
21
+ class LayerNotDefinedError < NameError
22
+ include Hypercuke::Error
23
+ end
24
+
25
+ class TopicNotDefinedError < NameError
26
+ include Hypercuke::Error
27
+ end
28
+
29
+ class StepAdapterNotDefinedError < NameError
30
+ include Hypercuke::Error
31
+
32
+ def self.translate_message(message)
33
+ step_adapter_name =
34
+ if md = /(Hypercuke::StepAdapters::\S*)/.match(message)
35
+ md.captures.first
36
+ else
37
+ message
38
+ end
39
+ "Step adapter not defined: '#{step_adapter_name}'"
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,9 @@
1
+ module Hypercuke
2
+ module MiniInflector
3
+ extend self
4
+
5
+ def camelize(name)
6
+ name.to_s.split('_').map(&:capitalize).join
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,38 @@
1
+ require 'forwardable'
2
+
3
+ module Hypercuke
4
+ class NameList
5
+ attr_reader :names
6
+ private :names
7
+
8
+ def initialize(*names)
9
+ @names = names.flatten
10
+ end
11
+
12
+ def define(new_name)
13
+ name = new_name.to_sym
14
+ names << name unless names.include?(name)
15
+ end
16
+
17
+ def validate(name)
18
+ if valid_name?(name)
19
+ return name.to_sym
20
+ else
21
+ yield if block_given?
22
+ return nil
23
+ end
24
+ end
25
+
26
+ def valid_name?(name)
27
+ names.include?(name.to_sym)
28
+ end
29
+
30
+ def to_a
31
+ names.dup
32
+ end
33
+
34
+ def empty?
35
+ names.empty?
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,12 @@
1
+ module Hypercuke
2
+ # I am the superclass for all of the generated step adapters.
3
+ class StepAdapter
4
+ def initialize(context, step_driver)
5
+ @context = context
6
+ @step_driver = step_driver
7
+ end
8
+
9
+ private
10
+ attr_reader :context, :step_driver
11
+ end
12
+ end
@@ -0,0 +1,126 @@
1
+ module Hypercuke
2
+ # Greetings, dear reader. There's a lot going in this file, so it's
3
+ # been organized in a more conversational style than most of the Ruby
4
+ # code I write. I hope it helps.
5
+
6
+ # First things first: the point of this entire file.
7
+ #
8
+ # Should you find yourself in possession of a [topic, layer] name
9
+ # pair, this method allows you to redeem it for VALUABLE PRIZES! (And
10
+ # by "valuable prizes", I mean "a reference to a step adapter class,
11
+ # if one has already been defined.")
12
+ #
13
+ # This method (on Hypercuke itself) is but a facade...
14
+ def self.step_adapter_class(topic_name, layer_name)
15
+ topic_module = StepAdapters.fetch_topic_module(topic_name)
16
+ topic_module.fetch_step_adapter(layer_name)
17
+ end
18
+
19
+ # We start out with a namespace for step adapters. As new step
20
+ # adapter classes are created (see Hypercuke::AdapterDefinition), they
21
+ # will be assigned to constants in this module's namespace (so that
22
+ # they can have human-friendly names when something goes wrong and
23
+ # #inspect gets called on them).
24
+ #
25
+ # Class names will be of the form:
26
+ # Hypercuke::StepAdapters::<TopicName>::<LayerName>
27
+ module StepAdapters
28
+ extend self # BTW, I hate typing "self." in modules.
29
+
30
+ # We'll get to what makes a topic module special is in a moment, but
31
+ # here's how we fetch one:
32
+ def fetch_topic_module(topic_name)
33
+ # FIXME: cyclomatic complexity
34
+ Hypercuke.topics.validate(topic_name) do
35
+ fail TopicNotDefinedError, "Topic not defined: #{topic_name}"
36
+ end
37
+ validate_topic_module \
38
+ begin
39
+ const_get( MiniInflector.camelize(topic_name) )
40
+ rescue NameError => e
41
+ raise Hypercuke::TopicNotDefinedError.wrap(e)
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def validate_topic_module(candidate)
48
+ return candidate if candidate.kind_of?(::Hypercuke::TopicModule)
49
+ fail Hypercuke::TopicNotDefinedError
50
+ end
51
+ end
52
+
53
+ # So, a TopicModule is (a) a namespace for holding step adapters, and
54
+ # (b) a slightly specialized type of Module that knows enough to be
55
+ # able to fetch step adapters.
56
+ class TopicModule < Module
57
+ def fetch_step_adapter(layer_name)
58
+ # FIXME: cyclomatic complexity
59
+ Hypercuke.layers.validate(layer_name) do
60
+ fail LayerNotDefinedError, "Layer not defined: #{layer_name}"
61
+ end
62
+ validate_step_adapter \
63
+ begin
64
+ const_get( MiniInflector.camelize(layer_name) )
65
+ rescue NameError => e
66
+ raise Hypercuke::StepAdapterNotDefinedError.wrap(e)
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def validate_step_adapter(candidate)
73
+ return candidate if candidate.kind_of?(Class) && candidate.ancestors.include?(::Hypercuke::StepAdapter)
74
+ fail Hypercuke::StepAdapterNotDefinedError
75
+ end
76
+ end
77
+
78
+ # Okay. That covers fetching step adapters once they've been defined.
79
+ # Now let's talk about how we define new ones.
80
+
81
+ module StepAdapters
82
+ # Here's the entry point for the adapter definition API. It's
83
+ # entirely possible that a user might want to define their step
84
+ # adapters in a re-entrant way (much like we've been reopening the
85
+ # same modules and classes in this file), so this bit of the code
86
+ # will either create a new step adapter, or return one that's
87
+ # already been defined.
88
+ def define(topic_name, layer_name)
89
+ topic_module = define_topic_module(topic_name)
90
+ step_adapter = topic_module.define_step_adapter( layer_name )
91
+ end
92
+ end
93
+
94
+ # The next two bits follow this pattern:
95
+ # 1) attempt to fetch the requested thing.
96
+ # 2) if fetch fails, define and return it.
97
+
98
+ module StepAdapters
99
+ def define_topic_module(topic_name)
100
+ fetch_topic_module(topic_name)
101
+ rescue Hypercuke::TopicNotDefinedError
102
+ const_name = MiniInflector.camelize(topic_name)
103
+ const_set const_name, TopicModule.new
104
+ end
105
+ end
106
+
107
+ class TopicModule < Module
108
+ def define_step_adapter(layer_name)
109
+ fetch_step_adapter(layer_name)
110
+ rescue Hypercuke::StepAdapterNotDefinedError
111
+ const_name = MiniInflector.camelize(layer_name)
112
+ const_set const_name, Class.new(StepAdapter)
113
+ end
114
+ end
115
+
116
+ # One final bit of business before we go: when testing code that
117
+ # defines classes and binds them to constants, it is occasionally
118
+ # useful to reset to a blank slate.
119
+ module StepAdapters
120
+ def clear
121
+ constants.each do |c|
122
+ remove_const c
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,52 @@
1
+ module Hypercuke
2
+
3
+ # The step driver serves as the entry point from Cucumber step
4
+ # definition bodies to the Hypercuke API. An instance of this object
5
+ # will be made available to Cucumber::World via the #step_driver
6
+ # method.
7
+ #
8
+ # The StepDriver will interpret any message sent to it as a topic
9
+ # name, combine that with the current_layer name from Hypercuke, and
10
+ # use that to instantitate a StepAdapter for the appropriate
11
+ # topic/layer combo.
12
+ class StepDriver
13
+ def layer_name
14
+ Hypercuke.current_layer
15
+ end
16
+
17
+ def method_missing(method, *_O) # No arguments for you, Mister Bond! *adjusts monocle*
18
+ topic_name = method.to_sym
19
+
20
+ # Define a method for the topic name so that future requests for
21
+ # the same step adapter don't pay the method_missing tax.
22
+ self.class.send(:define_method, topic_name) do
23
+ # Within the defined method, memoize the step adapter so that
24
+ # future requests also don't pay the GC tax.
25
+ key = [topic_name, layer_name] # key on both names in case someone changes the layer on us
26
+ __step_adapters__[key] ||=
27
+ begin
28
+ klass = Hypercuke.step_adapter_class(*key)
29
+ klass.new(__context__, self)
30
+ end
31
+ end
32
+
33
+ # And don't forget to invoke the newly-created method.
34
+ send(method)
35
+ end
36
+
37
+ # StepDriver is eager to please.
38
+ def respond_to_missing?(_)
39
+ true
40
+ end
41
+
42
+ private
43
+
44
+ def __context__
45
+ @__context__ ||= Hypercuke::Context.new
46
+ end
47
+
48
+ def __step_adapters__
49
+ @__step_adapters__ ||= {}
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,3 @@
1
+ module Hypercuke
2
+ VERSION = "0.4.1"
3
+ end