serf 0.10.0 → 0.11.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 (59) hide show
  1. data/.gitignore +21 -0
  2. data/.travis.yml +7 -0
  3. data/Gemfile +20 -26
  4. data/Guardfile +16 -0
  5. data/NOTICE.txt +1 -1
  6. data/README.md +223 -207
  7. data/Rakefile +3 -18
  8. data/lib/serf/builder.rb +31 -136
  9. data/lib/serf/errors/policy_failure.rb +10 -0
  10. data/lib/serf/middleware/error_handler.rb +53 -0
  11. data/lib/serf/middleware/parcel_freezer.rb +36 -0
  12. data/lib/serf/middleware/parcel_masher.rb +39 -0
  13. data/lib/serf/middleware/policy_checker.rb +31 -0
  14. data/lib/serf/middleware/uuid_tagger.rb +13 -11
  15. data/lib/serf/parcel_builder.rb +30 -0
  16. data/lib/serf/serfer.rb +27 -66
  17. data/lib/serf/util/error_handling.rb +13 -36
  18. data/lib/serf/util/protected_call.rb +2 -2
  19. data/lib/serf/util/uuidable.rb +14 -38
  20. data/lib/serf/version.rb +1 -1
  21. data/schemas/{caught_exception_event.json → serf/events/caught_error.json} +4 -7
  22. data/serf.gemspec +22 -101
  23. data/spec/serf/builder_spec.rb +44 -0
  24. data/spec/serf/errors/policy_failure_spec.rb +11 -0
  25. data/spec/serf/middleware/error_handler_spec.rb +48 -0
  26. data/spec/serf/middleware/parcel_freezer_spec.rb +20 -0
  27. data/spec/serf/middleware/parcel_masher_spec.rb +30 -0
  28. data/spec/serf/middleware/policy_checker_spec.rb +70 -0
  29. data/spec/serf/middleware/uuid_tagger_spec.rb +32 -0
  30. data/spec/serf/parcel_builder_spec.rb +46 -0
  31. data/spec/serf/serfer_spec.rb +61 -0
  32. data/spec/serf/util/error_handling_spec.rb +35 -0
  33. data/spec/serf/util/null_object_spec.rb +26 -0
  34. data/spec/serf/util/options_extraction_spec.rb +62 -0
  35. data/spec/serf/util/protected_call_spec.rb +33 -0
  36. data/spec/serf/util/uuidable_spec.rb +56 -0
  37. data/spec/serf_spec.rb +1 -4
  38. data/spec/spec_helper.rb +3 -0
  39. data/spec/support/error_handling_wrapper.rb +5 -0
  40. data/spec/support/factories.rb +32 -0
  41. data/spec/support/failing_policy.rb +9 -0
  42. data/spec/support/json_schema_tester.rb +14 -0
  43. data/spec/support/options_extraction_wrapper.rb +10 -0
  44. data/spec/support/passing_policy.rb +7 -0
  45. data/spec/support/protected_call_wrapper.rb +5 -0
  46. metadata +81 -131
  47. data/.document +0 -5
  48. data/.rspec +0 -1
  49. data/Gemfile.lock +0 -58
  50. data/docs/thread_pools.txt +0 -16
  51. data/lib/serf/command.rb +0 -79
  52. data/lib/serf/error.rb +0 -11
  53. data/lib/serf/errors/not_found.rb +0 -8
  54. data/lib/serf/middleware/girl_friday_async.rb +0 -39
  55. data/lib/serf/middleware/masherize.rb +0 -25
  56. data/lib/serf/routing/regexp_matcher.rb +0 -35
  57. data/lib/serf/routing/route.rb +0 -35
  58. data/lib/serf/routing/route_set.rb +0 -64
  59. data/schemas/message_accepted_event.json +0 -14
data/lib/serf/builder.rb CHANGED
@@ -1,166 +1,61 @@
1
- require 'serf/routing/route'
2
- require 'serf/routing/route_set'
1
+ require 'serf/middleware/error_handler'
2
+ require 'serf/middleware/parcel_freezer'
3
+ require 'serf/middleware/parcel_masher'
4
+ require 'serf/middleware/policy_checker'
5
+ require 'serf/middleware/uuid_tagger'
3
6
  require 'serf/serfer'
4
- require 'serf/util/null_object'
5
7
  require 'serf/util/options_extraction'
6
8
 
7
9
  module Serf
8
10
 
9
- ##
10
- # A Serf Builder that processes the SerfUp DSL to build a rack-like
11
- # app to handlers that process received messages. This builder is
12
- # implemented based on code from Rack::Builder.
13
- #
14
- # builder = Serf::Builder.parse_file 'examples/config.su'
15
- # builder.to_app
16
- #
17
- # or
18
- #
19
- # builder = Serf::Builder.new do
20
- # ... A SerfUp Config block here.
21
- # end
22
- # builder.to_app
23
- #
24
11
  class Builder
25
12
  include Serf::Util::OptionsExtraction
26
13
 
27
- attr_reader :serfer_factory
28
- attr_reader :route_set_factory
29
- attr_reader :route_factory
30
-
31
- def self.parse_file(config)
32
- cfgfile = ::File.read(config)
33
- builder = eval "Serf::Builder.new {\n" + cfgfile + "\n}",
34
- TOPLEVEL_BINDING, config
35
- return builder
36
- end
37
-
38
- def self.app(*args, &block)
39
- new(*args, &block).to_app
40
- end
41
-
42
14
  def initialize(*args, &block)
43
15
  extract_options! args
44
16
 
45
- # Our factories
46
- @serfer_factory = opts :serfer_factory, Serf::Serfer
47
- @route_set_factory = opts :route_set_factory, Serf::Routing::RouteSet
48
- @route_factory = opts :route_factory, Serf::Routing::Route
49
-
50
- # List of middleware to be executed (non-built form)
17
+ @run = opts :interactor
51
18
  @use = []
19
+ @policy_chain = opts :policy_chain, []
52
20
 
53
- # A list of "mounted", non-built, command handlers with their
54
- # matcher and policies.
55
- @runs = []
56
-
57
- # List of default policies to be run (non-built form)
58
- @default_policies = []
59
-
60
- # The current matcher
61
- @matcher = nil
62
-
63
- # Current policies to be run (PRE-built)
64
- @policies = []
65
-
66
- # configure based on a given block.
67
- instance_eval(&block) if block_given?
21
+ if block_given?
22
+ instance_eval(&block)
23
+ else
24
+ use_defaults
25
+ end
68
26
  end
69
27
 
70
28
  ##
71
- # Append a policy to default policy chain. The default
72
- # policy chain is used by any route that does not define
73
- # at least one of its own policies.
29
+ # Set a default chain of the following:
30
+ #
31
+ # use Serf::Middleware::ParcelMasher
32
+ # use Serf::Middleware::UuidTagger
33
+ # use Serf::Middleware::ParcelFreezer
34
+ # use Serf::Middleware::ErrorHandler
35
+ # use Serf::Middleware::PolicyChecker, @policy_chain
36
+ # use Serf::Serfer
74
37
  #
75
- # @param policy the policy factory to append
76
- # @param *args the arguments to pass to the factory
77
- # @param &block the block to pass to the factory
78
- def default_policy(policy, *args, &block)
79
- @default_policies << proc { policy.build(*args, &block) }
38
+ def use_defaults
39
+ use Serf::Middleware::ParcelMasher
40
+ use Serf::Middleware::UuidTagger
41
+ use Serf::Middleware::ParcelFreezer
42
+ use Serf::Middleware::ErrorHandler
43
+ use Serf::Middleware::PolicyChecker, policy_chain: @policy_chain
44
+ use Serf::Serfer
80
45
  end
81
46
 
82
- ##
83
- # Append a rack-like middleware
84
- #
85
- # @param the middleware class
86
- # @param *args the arguments to pass to middleware.new
87
- # @param &block the block to pass to middleware.new
88
47
  def use(middleware, *args, &block)
89
48
  @use << proc { |app| middleware.new(app, *args, &block) }
90
49
  end
91
50
 
92
- ##
93
- # Append a policy to the current match's policy chain.
94
- #
95
- # @param policy the policy factory to append
96
- # @param *args the arguments to pass to the factory
97
- # @param &block the block to pass to the factory
98
- def policy(policy, *args, &block)
99
- @policies << proc { policy.build(*args, &block) }
51
+ def run(interactor)
52
+ @run = interactor
100
53
  end
101
54
 
102
- def response_channel(channel); @response_channel = channel; end
103
- def error_channel(channel); @error_channel = channel; end
104
- def logger(logger); @logger = logger; end
105
-
106
- ##
107
- # DSL Method to change our current context to use the given matcher.
108
- #
109
- def match(matcher)
110
- @matcher = matcher
111
- @policies = []
112
- end
113
-
114
- ##
115
- # @param command_factory the factory to invoke (in #to_app)
116
- # @param *args the rest of the args to pass to command_factory#build method
117
- # @param &block the block to pass to command_factory#build method
118
- def run(command_factory, *args, &block)
119
- raise 'No matcher defined yet' unless @matcher
120
- # Create a local duplicate of the matcher and policies "snapshotted"
121
- # at the time this method is called... so that snapshot is consistent
122
- # for when the proc is called.
123
- matcher = @matcher.dup
124
- policies = @policies.dup
125
-
126
- # This proc will be called in to_app when we actually go ahead and
127
- # instantiate all the objects. By this point, route_set and
128
- # default_policies passed to this proc will be ready, built.
129
- @runs << proc { |route_set, default_policies|
130
- route_set.add(
131
- matcher,
132
- route_factory.build(
133
- command: command_factory.build(*args, &block),
134
- policies: (policies.size > 0 ?
135
- policies.map{ |p| p.call } :
136
- default_policies)))
137
- }
138
- end
139
-
140
- ##
141
- # Create our app.
142
- #
143
55
  def to_app
144
- # Create the route_set to resolve routes
145
- route_set = route_set_factory.build
146
- # Build the default policies to be used if routes did not specify any.
147
- default_policies = @default_policies.map{ |p| p.call }
148
- # Add each route to the route_set
149
- for run in @runs
150
- run.call route_set, default_policies
151
- end
152
- # Create our serfer class
153
- app = serfer_factory.build(
154
- route_set: route_set,
155
- response_channel: (@response_channel || Serf::Util::NullObject.new),
156
- error_channel: (@error_channel || Serf::Util::NullObject.new),
157
- logger: (@logger || Serf::Util::NullObject.new))
158
-
159
- # We're going to inject middleware here.
160
- app = @use.reverse.inject(app) { |a,e| e[a] } if @use.size > 0
161
-
162
- return app
56
+ @use.reverse.inject(@run) { |a,e| e[a] }
163
57
  end
164
58
 
165
59
  end
60
+
166
61
  end
@@ -0,0 +1,10 @@
1
+ module Serf
2
+ module Errors
3
+
4
+ ##
5
+ # Common base error to raise for any policy failure.
6
+ class PolicyFailure < RuntimeError
7
+ end
8
+
9
+ end
10
+ end
@@ -0,0 +1,53 @@
1
+ require 'hashie'
2
+
3
+ require 'serf/parcel_builder'
4
+ require 'serf/util/error_handling'
5
+ require 'serf/util/uuidable'
6
+
7
+ module Serf
8
+ module Middleware
9
+
10
+ ##
11
+ # Middleware to catch raised exceptions and return an error parcel
12
+ # instead.
13
+ #
14
+ class ErrorHandler
15
+ include Serf::Util::ErrorHandling
16
+ include Serf::Util::OptionsExtraction
17
+
18
+ attr_reader :app
19
+ attr_reader :parcel_builder
20
+ attr_reader :uuidable
21
+
22
+ ##
23
+ # @param app the app
24
+ #
25
+ def initialize(app, *args)
26
+ extract_options! args
27
+ @app = app
28
+
29
+ # Tunable knobs
30
+ @parcel_builder = opts(:parcel_builder) { Serf::ParcelBuilder.new }
31
+ @uuidable = opts(:uuidable) { Serf::Util::Uuidable.new }
32
+ end
33
+
34
+ def call(parcel)
35
+ # Attempt to execute the app, catching errors
36
+ response_parcel, error_message = with_error_handling do
37
+ app.call parcel
38
+ end
39
+
40
+ # Return on success
41
+ return response_parcel if response_parcel
42
+
43
+ # We got an error message instead, so build out the headers
44
+ # and return the parcel.
45
+ error_headers = uuidable.create_uuids parcel[:headers]
46
+ error_headers[:kind] = 'serf/events/caught_error'
47
+ return parcel_builder.build error_headers, error_message
48
+ end
49
+
50
+ end
51
+
52
+ end
53
+ end
@@ -0,0 +1,36 @@
1
+ require 'ice_nine'
2
+
3
+ require 'serf/util/options_extraction'
4
+
5
+ module Serf
6
+ module Middleware
7
+
8
+ ##
9
+ # Middleware to add uuids to the headers of the parcel hash.
10
+ #
11
+ class ParcelFreezer
12
+ include Serf::Util::OptionsExtraction
13
+
14
+ attr_reader :app
15
+ attr_reader :freezer
16
+
17
+ ##
18
+ # @param app the app
19
+ #
20
+ def initialize(app, *args)
21
+ extract_options! args
22
+ @app = app
23
+ @freezer = opts :freezer, IceNine
24
+ end
25
+
26
+ ##
27
+ # Chains the call, but deep freezes the parcel.
28
+ def call(parcel)
29
+ freezer.deep_freeze parcel
30
+ app.call parcel
31
+ end
32
+
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,39 @@
1
+ require 'hashie'
2
+
3
+ require 'serf/util/options_extraction'
4
+
5
+ module Serf
6
+ module Middleware
7
+
8
+ ##
9
+ # Middleware to add uuids to the headers of the parcel hash.
10
+ #
11
+ class ParcelMasher
12
+ include Serf::Util::OptionsExtraction
13
+
14
+ attr_reader :app
15
+ attr_reader :masher_class
16
+
17
+ ##
18
+ # @param app the app
19
+ #
20
+ def initialize(app, *args)
21
+ extract_options! args
22
+ @app = app
23
+ @masher_class = opts :masher_class, Hashie::Mash
24
+ end
25
+
26
+ ##
27
+ # Coerces the parcel into a Hashie::Mash, makes sure that
28
+ # the headers and message are set, and then passes it along the chain.
29
+ def call(parcel)
30
+ mashed_parcel = masher_class.new parcel
31
+ mashed_parcel[:headers] ||= {}
32
+ mashed_parcel[:message] ||= {}
33
+ app.call mashed_parcel
34
+ end
35
+
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,31 @@
1
+ require 'serf/util/options_extraction'
2
+
3
+ module Serf
4
+ module Middleware
5
+
6
+ class PolicyChecker
7
+ include Serf::Util::OptionsExtraction
8
+
9
+ attr_reader :app
10
+ attr_reader :policy_chain
11
+
12
+ def initialize(app, *args)
13
+ extract_options! args
14
+ @app = app
15
+ @policy_chain = opts :policy_chain, []
16
+ end
17
+
18
+ ##
19
+ # Iterates the policy chain and does a check for each policy.
20
+ # Assumes that policies will raise errors on any policy failure.
21
+ def call(parcel)
22
+ policy_chain.each do |policy|
23
+ policy.check! parcel
24
+ end
25
+ app.call parcel
26
+ end
27
+
28
+ end
29
+
30
+ end
31
+ end
@@ -1,36 +1,38 @@
1
+ require 'hashie'
2
+
3
+ require 'serf/util/options_extraction'
1
4
  require 'serf/util/uuidable'
2
5
 
3
6
  module Serf
4
7
  module Middleware
5
8
 
6
9
  ##
7
- # Middleware to add a request uuid to both the message and context
8
- # of the env hash. But it won't overwrite the uuid field
9
- # if the incoming request already has it.
10
+ # Middleware to add uuids to the headers of the parcel hash.
10
11
  #
11
12
  class UuidTagger
12
13
  include Serf::Util::OptionsExtraction
13
14
 
15
+ attr_reader :app
14
16
  attr_reader :uuidable
15
17
 
16
18
  ##
17
19
  # @param app the app
18
- # @options opts [String] :field the ENV field to set with a UUID.
19
20
  #
20
21
  def initialize(app, *args)
21
22
  extract_options! args
22
23
  @app = app
23
- @uuidable = opts :uuidable, Serf::Util::Uuidable
24
+ @uuidable = opts(:uuidable) { Serf::Util::Uuidable.new }
24
25
  end
25
26
 
26
- def call(env)
27
- message = env[:message]
28
- message[:uuid] = uuidable.create_coded_uuid if message && !message[:uuid]
27
+ def call(parcel)
28
+ # Makes sure our parcel has headers
29
+ parcel[:headers] ||= {}
29
30
 
30
- context = env[:context]
31
- context[:uuid] = uuidable.create_coded_uuid if context && !context[:uuid]
31
+ # Tag headers with a UUID unless it already has one
32
+ parcel[:headers][:uuid] ||= uuidable.create_coded_uuid
32
33
 
33
- @app.call env
34
+ # Pass on the given parcel with newly annotated headers
35
+ app.call parcel
34
36
  end
35
37
 
36
38
  end
@@ -0,0 +1,30 @@
1
+ require 'hashie'
2
+
3
+ require 'serf/util/options_extraction'
4
+
5
+ module Serf
6
+
7
+ ##
8
+ # Builds Parcels as Hashie::Mash objects with headers and messages.
9
+ #
10
+ class ParcelBuilder
11
+ include Serf::Util::OptionsExtraction
12
+
13
+ attr_reader :mash_class
14
+
15
+ def initialize(*args)
16
+ extract_options! args
17
+
18
+ @mash_class = opts :mash_class, Hashie::Mash
19
+ end
20
+
21
+ def build(headers=nil, message=nil)
22
+ # We want to make sure that our headers and message are Mashes.
23
+ headers = mash_class.new(headers) unless headers.kind_of? mash_class
24
+ message = mash_class.new(message) unless message.kind_of? mash_class
25
+ mash_class.new headers: headers, message: message
26
+ end
27
+
28
+ end
29
+
30
+ end
data/lib/serf/serfer.rb CHANGED
@@ -1,87 +1,48 @@
1
1
  require 'hashie'
2
2
 
3
- require 'serf/error'
4
- require 'serf/errors/not_found'
5
- require 'serf/util/error_handling'
6
- require 'serf/util/null_object'
3
+ require 'serf/parcel_builder'
4
+ require 'serf/util/options_extraction'
5
+ require 'serf/util/uuidable'
7
6
 
8
7
  module Serf
9
8
 
10
9
  ##
11
- # Class to drive the command handler execution, error handling, etc
12
- # of received messages.
10
+ # Class to drive the Interactor execution.
11
+ #
13
12
  class Serfer
14
- include Serf::Util::ErrorHandling
13
+ include Serf::Util::OptionsExtraction
15
14
 
16
- attr_reader :route_set
17
- attr_reader :response_channel
18
- attr_reader :error_channel
19
- attr_reader :logger
15
+ attr_reader :interactor
16
+ attr_reader :parcel_builder
17
+ attr_reader :uuidable
20
18
 
21
- def initialize(*args)
19
+ def initialize(interactor, *args)
22
20
  extract_options! args
23
21
 
24
- @route_set = opts! :route_set
25
- @response_channel = opts(:response_channel) { Serf::Util::NullObject }
26
- @error_channel = opts(:error_channel) { Serf::Util::NullObject }
27
- @logger = opts(:logger) { Serf::Util::NullObject }
22
+ # How to and when to handle requests
23
+ @interactor = interactor
24
+
25
+ # Tunable knobs
26
+ @parcel_builder = opts(:parcel_builder) { Serf::ParcelBuilder.new }
27
+ @uuidable = opts(:uuidable) { Serf::Util::Uuidable.new }
28
28
  end
29
29
 
30
30
  ##
31
- # Rack-like call to run a set of handlers for a message
31
+ # Rack-like call to run the Interactor's use-case.
32
32
  #
33
- def call(env)
34
- env = Hashie::Mash.new env unless env.is_a? Hashie::Mash
35
-
36
- # We normalize by making the request a Hashie Mash
37
- message = Hashie::Mash.new env.message
38
- context = Hashie::Mash.new env.context
39
-
40
- # Resolve the routes that we want to run
41
- routes = route_set.resolve message, context
42
-
43
- # We raise an error if no routes were found.
44
- raise Serf::Errors::NotFound unless routes.size > 0
33
+ def call(parcel)
34
+ headers = parcel[:headers]
35
+ message = parcel[:message]
45
36
 
46
- # For each route, we're going to execute
47
- results = routes.map { |route|
48
- # 1. Check request+context with the policies (RAISE)
49
- # 2. Execute command (RETURNS Hash)
50
- ok, res = with_error_handling(
51
- message: message,
52
- options: context) do
53
- route.check_policies! message, context
54
- route.execute! message, context
55
- end
56
- # Return the run_results as result of this block.
57
- res
58
- }.flatten.select { |r| r }
59
- push_results results, context
60
- return results
61
- rescue => e
62
- e.extend(Serf::Error)
63
- raise e
64
- end
65
-
66
- def self.build(*args, &block)
67
- new *args, &block
68
- end
37
+ # 1. Execute interactor
38
+ response_message, response_kind = interactor.call message
69
39
 
70
- private
40
+ # 2. Create the response headers
41
+ response_headers = uuidable.create_uuids headers
42
+ response_headers[:kind] = response_kind
71
43
 
72
- ##
73
- # Loop over the results and push them to the response channel.
74
- # Any error in pushing individual messages will result in
75
- # a log event and an error channel event.
76
- def push_results(results, context)
77
- results.each do |result|
78
- with_error_handling(result) do
79
- response_channel.push(
80
- message: result,
81
- context: context)
82
- end
83
- end
84
- return nil
44
+ # 3. Return the response headers and message as a parcel
45
+ return parcel_builder.build response_headers, response_message
85
46
  end
86
47
 
87
48
  end
@@ -1,7 +1,3 @@
1
- require 'active_support/core_ext/string/inflections'
2
-
3
- require 'serf/util/null_object'
4
- require 'serf/util/options_extraction'
5
1
  require 'serf/util/protected_call'
6
2
 
7
3
  module Serf
@@ -9,53 +5,34 @@ module Util
9
5
 
10
6
  ##
11
7
  # Helper module to rescues exceptions from executing blocks of
12
- # code, and then logs+pushes the error event.
8
+ # code, and then converts the exception to an "Error Message".
13
9
  #
14
- # Including classes may have the following instance variables
15
- # to override the default values:
16
- # * @logger - ::Serf::Util::NullObject.new
17
- # * @error_channel - ::Serf::Util::NullObject.new
18
10
  module ErrorHandling
19
- include Serf::Util::OptionsExtraction
20
11
  include Serf::Util::ProtectedCall
21
12
 
22
13
  ##
23
14
  # A block wrapper to handle errors when executing a block.
24
15
  #
25
- def with_error_handling(context=nil, *args, &block)
26
- ok, results = pcall *args, &block
27
- return ok, (ok ? results : handle_error(results, context))
16
+ def with_error_handling(*args, &block)
17
+ results, err = pcall *args, &block
18
+ return results, handle_error(err)
28
19
  end
29
20
 
30
21
  ##
31
22
  # Including classes may override this method to do alternate error
32
- # handling. By default, this method will create a new caught exception
33
- # event and publish it to the error channel. This method will also
34
- # log the exception itself to the logger.
23
+ # handling. By default, this method will create a new error event message.
35
24
  #
36
- def handle_error(e, context=nil)
37
- logger = opts(:logger){ ::Serf::Util::NullObject.new }
38
- error_channel = opts(:error_channel) { ::Serf::Util::NullObject.new }
39
- error_event = {
40
- kind: 'serf/messages/caught_exception_event',
41
- context: context,
42
- error: e.class.to_s.underscore,
25
+ def handle_error(e)
26
+ # no error was passed, so do nothing.
27
+ return nil unless e
28
+
29
+ # Return a simple error event message
30
+ return {
31
+ error: e.class.to_s,
43
32
  message: e.message,
33
+ process_env: ENV.to_hash,
44
34
  backtrace: e.backtrace.join("\n")
45
35
  }
46
-
47
- # log the error to our logger
48
- logger.error e
49
-
50
- # log the error event to our error channel.
51
- begin
52
- error_channel.push error_event
53
- rescue => e1
54
- logger.error e1
55
- end
56
-
57
- # We're done, so just return this error.
58
- return error_event
59
36
  end
60
37
 
61
38
  end
@@ -24,9 +24,9 @@ module Util
24
24
  # @return boolean success and the block's (or caught exception) results.
25
25
  #
26
26
  def pcall(*args)
27
- return true, yield(*args)
27
+ return yield(*args), nil
28
28
  rescue => e
29
- return false, e
29
+ return nil, e
30
30
  end
31
31
  alias_method :protected_call, :pcall
32
32