circuit 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. data/Gemfile +34 -0
  2. data/LICENSE +20 -0
  3. data/README.md +161 -0
  4. data/Rakefile +27 -0
  5. data/config.ru +7 -0
  6. data/description.md +5 -0
  7. data/docs/COMPATIBILITY.md +14 -0
  8. data/docs/ROADMAP.md +29 -0
  9. data/lib/circuit.rb +125 -0
  10. data/lib/circuit/behavior.rb +99 -0
  11. data/lib/circuit/compatibility.rb +73 -0
  12. data/lib/circuit/middleware.rb +6 -0
  13. data/lib/circuit/middleware/rewriter.rb +43 -0
  14. data/lib/circuit/rack.rb +14 -0
  15. data/lib/circuit/rack/behavioral.rb +45 -0
  16. data/lib/circuit/rack/builder.rb +50 -0
  17. data/lib/circuit/rack/multi_site.rb +22 -0
  18. data/lib/circuit/rack/request.rb +81 -0
  19. data/lib/circuit/railtie.rb +24 -0
  20. data/lib/circuit/storage.rb +74 -0
  21. data/lib/circuit/storage/memory_model.rb +70 -0
  22. data/lib/circuit/storage/nodes.rb +56 -0
  23. data/lib/circuit/storage/nodes/memory_store.rb +63 -0
  24. data/lib/circuit/storage/nodes/model.rb +67 -0
  25. data/lib/circuit/storage/nodes/mongoid_store.rb +56 -0
  26. data/lib/circuit/storage/sites.rb +38 -0
  27. data/lib/circuit/storage/sites/memory_store.rb +53 -0
  28. data/lib/circuit/storage/sites/model.rb +29 -0
  29. data/lib/circuit/storage/sites/mongoid_store.rb +43 -0
  30. data/lib/circuit/validators.rb +74 -0
  31. data/lib/circuit/version.rb +3 -0
  32. data/spec/internal/app/behaviors/change_path.rb +7 -0
  33. data/spec/internal/app/controllers/application_controller.rb +5 -0
  34. data/spec/internal/app/helpers/application_helper.rb +2 -0
  35. data/spec/internal/config/initializers/circuit.rb +7 -0
  36. data/spec/internal/config/routes.rb +3 -0
  37. data/spec/internal/db/schema.rb +1 -0
  38. data/spec/lib/circuit/behavior_spec.rb +113 -0
  39. data/spec/lib/circuit/middleware/rewriter_spec.rb +79 -0
  40. data/spec/lib/circuit/rack/behavioral_spec.rb +60 -0
  41. data/spec/lib/circuit/rack/builder_spec.rb +125 -0
  42. data/spec/lib/circuit/rack/multi_site_spec.rb +34 -0
  43. data/spec/lib/circuit/rack/request_spec.rb +80 -0
  44. data/spec/lib/circuit/railtie_spec.rb +34 -0
  45. data/spec/lib/circuit/storage/nodes_spec.rb +62 -0
  46. data/spec/lib/circuit/storage/sites_spec.rb +60 -0
  47. data/spec/lib/circuit/storage_spec.rb +20 -0
  48. data/spec/lib/circuit/validators_spec.rb +69 -0
  49. data/spec/lib/circuit_spec.rb +139 -0
  50. data/spec/spec_helper.rb +79 -0
  51. data/spec/support/blueprints.rb +24 -0
  52. data/spec/support/matchers/be_current_time_matcher.rb +14 -0
  53. data/spec/support/matchers/extended_have_key.rb +31 -0
  54. data/spec/support/matchers/have_accessor_matcher.rb +13 -0
  55. data/spec/support/matchers/have_attribute_matcher.rb +22 -0
  56. data/spec/support/matchers/have_block_matcher.rb +14 -0
  57. data/spec/support/matchers/have_errors_on_matcher.rb +30 -0
  58. data/spec/support/matchers/have_module_matcher.rb +13 -0
  59. data/spec/support/matchers/have_reader_matcher.rb +18 -0
  60. data/spec/support/matchers/have_writer_matcher.rb +18 -0
  61. data/spec/support/matchers/set_instance_variable.rb +32 -0
  62. data/spec/support/spec_helpers/base_behaviors.rb +20 -0
  63. data/spec/support/spec_helpers/base_models.rb +64 -0
  64. data/spec/support/spec_helpers/logger_helpers.rb +58 -0
  65. data/spec/support/spec_helpers/multi_site_helper.rb +48 -0
  66. data/spec/support/spec_helpers/rack_helpers.rb +8 -0
  67. data/spec/support/spec_helpers/shared_examples/node_store.rb +87 -0
  68. data/spec/support/spec_helpers/shared_examples/site_store.rb +76 -0
  69. data/spec/support/spec_helpers/simple_machinable.rb +29 -0
  70. data/spec/support/spec_helpers/stores_cleaner.rb +46 -0
  71. data/vendor/active_support-3.2/core_ext/string/inflections.rb +53 -0
  72. data/vendor/active_support-3.2/inflector/methods.rb +65 -0
  73. data/vendor/rack-1.4/builder.rb +167 -0
  74. data/vendor/rack-1.4/urlmap.rb +96 -0
  75. metadata +238 -0
@@ -0,0 +1,73 @@
1
+ require 'rack'
2
+ require 'active_support'
3
+ require 'active_support/concern'
4
+ require 'active_support/core_ext/kernel/reporting'
5
+ require 'dionysus/string/version_match'
6
+
7
+ module Circuit
8
+ # Compatibility extensions for Rack 1.3 and Rails 3.1
9
+ module Compatibility
10
+ # Make Rack 1.3 and ActiveSupport 3.1 compatible with circuit.
11
+ # * Overrides Rack::Builder and Rack::URLMap with the classes from Rack 1.4
12
+ # * Adds #demodulize and #deconstantize inflections to ActiveSupport
13
+ def self.make_compatible
14
+ rack13 if ::Rack.release.version_match?("~> 1.3.0")
15
+ active_support31 if ActiveSupport::VERSION::STRING.version_match?("~> 3.1.0")
16
+ end
17
+
18
+ # Include in a model to modify it for compatibility.
19
+ module ActiveModel31
20
+ extend ActiveSupport::Concern
21
+
22
+ # @!method define_attribute_methods(*args)
23
+ # Modified to call `attribute_method_suffix ''` first to create the
24
+ # accessors.
25
+ # @see http://rubydoc.info/gems/activemodel/ActiveModel/AttributeMethods/ClassMethods#define_attribute_methods-instance_method
26
+ # @see http://rubydoc.info/gems/activemodel/ActiveModel/AttributeMethods/ClassMethods#attribute_method_suffix-instance_method
27
+
28
+ included do
29
+ if ActiveModel::VERSION::STRING.version_match?("~> 3.1.0")
30
+ if has_active_model_module?("AttributeMethods")
31
+ class << self
32
+ alias_method_chain :define_attribute_methods, :default_accessor
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ module ClassMethods
41
+ def has_active_model_module?(mod_name)
42
+ included_modules.detect {|mod| mod.to_s == "ActiveModel::#{mod_name.to_s.camelize}"}
43
+ end
44
+
45
+ def define_attribute_methods_with_default_accessor(*args)
46
+ attribute_method_suffix ''
47
+ define_attribute_methods_without_default_accessor(*args)
48
+ end
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def self.vendor_path
55
+ Circuit.vendor_path
56
+ end
57
+
58
+ def self.rack13
59
+ require "rack/urlmap"
60
+ require "rack/builder"
61
+
62
+ require vendor_path.join("rack-1.4", "builder").to_s
63
+ silence_warnings { require vendor_path.join("rack-1.4", "urlmap").to_s }
64
+ end
65
+
66
+ def self.active_support31
67
+ require "active_support/inflector"
68
+
69
+ require vendor_path.join("active_support-3.2", "inflector", "methods").to_s
70
+ require vendor_path.join("active_support-3.2", "core_ext", "string", "inflections").to_s
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,6 @@
1
+ module Circuit
2
+ # Set of predefined middlewares to use with Circuit
3
+ module Middleware
4
+ autoload :Rewriter, "circuit/middleware/rewriter"
5
+ end
6
+ end
@@ -0,0 +1,43 @@
1
+ module Circuit
2
+ module Middleware
3
+ # Raise if rewriting fails.
4
+ class RewriteError < CircuitError ; end
5
+
6
+ # Rewriter middleware
7
+ # @example Use the middleware (in rackup)
8
+ # use Rewriter do |script_name, path_info|
9
+ # ["/pages", script_name+path_info]
10
+ # end
11
+ # @example Use the middleware with Rack::Request object (in rackup)
12
+ # use Rewriter do |request|
13
+ # ["/site/#{request.site.id}"+request.script_name, request.path_info]
14
+ # end
15
+ # @see http://rubydoc.info/gems/rack/Rack/Request Rack::Request documentation
16
+ class Rewriter
17
+ # @param [#call] app Rack app
18
+ # @yield [script_name, path_info] `SCRIPT_NAME` and `PATH_INF`O values
19
+ # @yield [Request] `Rack::Request` object
20
+ # @yieldreturn [Array<String>] new `script_name` and `path_info`
21
+ def initialize(app, &block)
22
+ @app = app
23
+ @block = block
24
+ end
25
+
26
+ # Executes the rewrite
27
+ def call(env)
28
+ begin
29
+ request = ::Rack::Request.new(env)
30
+ script_name, path_info, path = request.script_name, request.path_info, request.path
31
+ env["SCRIPT_NAME"], env["PATH_INFO"] = @block.call(script_name, path_info)
32
+ if script_name != env["SCRIPT_NAME"] or path_info != env["PATH_INFO"]
33
+ ::Circuit.logger.info("[CIRCUIT] Rewriting: '#{path}'->'#{request.path}'")
34
+ end
35
+ rescue RewriteError => ex
36
+ headline = "[CIRCUIT] Rewrite Error"
37
+ ::Circuit.logger.error("%s: %s\n%s %s"%[headline, ex.message, " "*headline.length, ex.backtrace.first])
38
+ end
39
+ @app.call(env)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,14 @@
1
+ require 'rack/request'
2
+
3
+ module Circuit
4
+ module Rack
5
+ require 'circuit/rack/request'
6
+ ::Rack::Request.send(:include, Request)
7
+
8
+ require 'circuit/rack/builder'
9
+ ::Rack::Builder.send(:include, BuilderExt)
10
+
11
+ autoload :Behavioral, 'circuit/rack/behavioral'
12
+ autoload :MultiSite, 'circuit/rack/multi_site'
13
+ end
14
+ end
@@ -0,0 +1,45 @@
1
+ module Circuit
2
+ module Rack
3
+ # Raised if the `rack.circuit.site` rack variable is not defined (or `nil`)
4
+ class MissingSiteError < CircuitError; end
5
+
6
+ # Finds the route (Array of Nodes) for the request, and executes the
7
+ # route's behavior.
8
+ class Behavioral
9
+ def initialize(app)
10
+ @app = app
11
+ end
12
+
13
+ def call(env)
14
+ request = ::Rack::Request.new(env)
15
+
16
+ unless request.site
17
+ raise MissingSiteError, "Rack variable %s is missing"%[::Rack::Request::ENV_SITE]
18
+ end
19
+
20
+ remap! request
21
+ request.route.last.behavior.builder.tap do |builder|
22
+ builder.run(@app) unless builder.app?
23
+ end.call(env)
24
+ end
25
+
26
+ private
27
+
28
+ def remap(request)
29
+ route = ::Circuit.node_store.get(request.site, request.path)
30
+ return nil if route.blank?
31
+
32
+ request.route = route
33
+ return request
34
+ end
35
+
36
+ def remap!(request)
37
+ if result = remap(request)
38
+ result
39
+ else
40
+ raise ::Circuit::Storage::Nodes::NotFoundError, "Path not found"
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,50 @@
1
+ require 'rack/server'
2
+ require 'rack/builder'
3
+
4
+ module Circuit
5
+ module Rack
6
+ # Extensions to Rack::Builder
7
+ module BuilderExt
8
+ # @return [Boolean] true if a default app is set
9
+ def app?
10
+ !!@run
11
+ end
12
+
13
+ # Duplicates the `@use` Array and `@map` Hash instance variables
14
+ def initialize_copy(other)
15
+ @use = other.instance_variable_get(:@use).dup
16
+ unless other.instance_variable_get(:@map).nil?
17
+ @map = other.instance_variable_get(:@map).dup
18
+ end
19
+ end
20
+ end
21
+
22
+ # A Rack::Builder variation that does not require a fully-compliant rackup
23
+ # file; specifically that a default app (`run` directive) is not required.
24
+ class Builder < ::Rack::Builder
25
+ include BuilderExt
26
+
27
+ # Parses the rackup (or circuit-rackup .cru) file.
28
+ # @return [Circuit::Rack::Builder] the builder
29
+ def self.parse_file(config, opts = ::Rack::Server::Options.new)
30
+ # allow for objects that are String-like but don't respond to =~
31
+ # (e.g. Pathname)
32
+ config = config.to_s
33
+
34
+ if config.to_s =~ /\.cru$/
35
+ options = {}
36
+ cfgfile = ::File.read(config)
37
+ cfgfile.sub!(/^__END__\n.*\Z/m, '')
38
+ builder = eval "%s.new {\n%s\n}"%[self, cfgfile], TOPLEVEL_BINDING, config
39
+ return builder, options
40
+ else
41
+ # this should be a fully-compliant rackup file (or a constant name),
42
+ # so use the real Rack::Builder, but return a Builder object instead
43
+ # of the app
44
+ app, options = ::Rack::Builder.parse_file(config, opts)
45
+ return self.new(app), options
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,22 @@
1
+ module Circuit
2
+ module Rack
3
+ # Finds the Circuit::Site for the request. Returns a 404 if the site is not found.
4
+ class MultiSite
5
+ def initialize(app)
6
+ @app = app
7
+ end
8
+
9
+ def call(env)
10
+ request = ::Rack::Request.new(env)
11
+
12
+ request.site = Circuit.site_store.get(request.host)
13
+ unless request.site
14
+ # TODO custom 404 page for site not found
15
+ return [404, {}, ["Not Found"]]
16
+ end
17
+
18
+ @app.call(request.env)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,81 @@
1
+ require 'active_support/concern'
2
+
3
+ module Circuit
4
+ module Rack
5
+ # Extensions made to the ::Rack::Request class
6
+ # @see http://rubydoc.info/gems/rack/Rack/Request ::Rack::Request API documentation
7
+ # @see ClassMethods
8
+ module Request
9
+ extend ActiveSupport::Concern
10
+
11
+ # Key for storing the Circuit::Site instance
12
+ ENV_SITE = 'rack.circuit.site'
13
+
14
+ # Key for storing the array of Circuit::Node instances
15
+ ENV_ROUTE = 'rack.circuit.route'
16
+
17
+ # Key for storing original parameters that Circuit modifies.
18
+ ENV_ORIGINALS = 'rack.circuit.originals'
19
+
20
+ module ClassMethods
21
+ # Parses a URI path String into its segments
22
+ # @param [String] path to parse
23
+ # @return [Array<String>] segment parts; first segment will be `nil` if
24
+ # it is the root (i.e. the path is absolute)
25
+ # @see http://tools.ietf.org/html/rfc2396#section-3.3 RFC 2396: Uniform
26
+ # Resource Identifiers (URI): Generic Syntax - Path Component
27
+ def path_segments(path)
28
+ path.gsub(/^\/+/,'').split(/\/+/).select { |seg| !seg.blank? }.tap do |result|
29
+ result.unshift nil if path =~ /^\//
30
+ end
31
+ end
32
+ end
33
+
34
+ # @return [Circuit::Site] site from the environment
35
+ def site() @env[ENV_SITE]; end
36
+
37
+ # @param [Circuit::Site] site to set into the environment
38
+ def site=(site) @env[ENV_SITE] = site; end
39
+
40
+ # @return [Array<Circuit::Node>] the array of nodes that make up the
41
+ # route
42
+ def route() @env[ENV_ROUTE]; end
43
+
44
+ # Sets the route and modifies the `PATH_INFO` and `SCRIPT_NAME` variables
45
+ # to conform to the route. This means that that route's path will be set
46
+ # as the `SCRIPT_NAME` and the remainder of the path will be set as the
47
+ # `PATH_INFO`. Also, the original `PATH_INFO` and `SCRIPT_NAME`
48
+ # values are copied into the originals key.
49
+ # @param [Array<Circuit::Node>] route the array of nodes that make up the route
50
+ def route=(route)
51
+ raise(Circuit::Error, "Route has already been set") if self.route
52
+ save_original_path_envs
53
+ @env["PATH_INFO"] = "/"+path_segments[route.length..-1].join("/")
54
+ @env["PATH_INFO"] = "" if @env["PATH_INFO"] == "/"
55
+ @env["SCRIPT_NAME"] = route.last.path
56
+ @env[ENV_ROUTE] = route;
57
+ end
58
+
59
+ # @return [String] the route's path (aka. the `SCRIPT_NAME`)
60
+ def route_path() @env["SCRIPT_NAME"]; end
61
+
62
+ # @return [Hash] the originals of any keys that Circuit modifies are
63
+ # first copied into this Hash
64
+ def circuit_originals() @env[ENV_ORIGINALS] ||= {}; end
65
+
66
+ # @return [Array<String>] segment parts; first segment will be `nil` if
67
+ # it is the root (i.e. the path is absolute)
68
+ # @see ClassMethods#path_segments
69
+ def path_segments
70
+ self.class.path_segments self.path
71
+ end
72
+
73
+ # Saves the original path keys into #circuit_originals (i.e. `PATH_INFO`
74
+ # and `SCRIPT_NAME`)
75
+ def save_original_path_envs
76
+ circuit_originals["PATH_INFO"] = @env["PATH_INFO"]
77
+ circuit_originals["SCRIPT_NAME"] = @env["SCRIPT_NAME"]
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,24 @@
1
+ module Circuit
2
+ # Initializer `circuit_railtie.configure_rails_initialization`
3
+ # ------------------------------------------------------------
4
+ # * adds Rack::MultiSite and Rack::Behavioral to the Rails middleware stack
5
+ # * Sets the Circuit logger to the Rails logger
6
+ class Railtie < Rails::Railtie
7
+ initializer "circuit_railtie.configure_rails_initialization" do |app|
8
+ app.middleware.insert 0, Rack::MultiSite
9
+ app.middleware.insert 1, Rack::Behavioral
10
+
11
+ unless Circuit.cru_path
12
+ Circuit.cru_path = app.root.join("app", "behaviors")
13
+ end
14
+
15
+ root = app.root.expand_path.to_s
16
+ rel = Circuit.cru_path.expand_path.to_s.gsub(/^#{Regexp.escape(root)}\//, "")
17
+ app.config.paths.add rel, :eager_load => true, :glob => "**/*.rb"
18
+
19
+ app.config.after_initialize do
20
+ Circuit.logger = Rails.logger
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,74 @@
1
+ require 'active_support/inflector'
2
+
3
+ module Circuit
4
+ module Storage
5
+ autoload :MemoryModel, 'circuit/storage/memory_model'
6
+ autoload :Sites, 'circuit/storage/sites'
7
+ autoload :Nodes, 'circuit/storage/nodes'
8
+
9
+ # Raised if a storage instance is undefined.
10
+ class InstanceUndefinedError < CircuitError
11
+ def initialize(msg="Storage instance is undefined.")
12
+ super(msg)
13
+ end
14
+ end
15
+
16
+ # @raise InstanceUndefinedError if the instance isn't defined
17
+ # @return [Circuit::Storage::Nodes::BaseStore,Circuit::Storage::Sites::BaseStore]
18
+ # the storage instance
19
+ def instance
20
+ @instance || raise(InstanceUndefinedError)
21
+ end
22
+
23
+ # Set the storage instance and alias the `Node` or `Site` model under
24
+ # `Circuit` as `Circuit::Node` or `Circuit::Site`.
25
+ #
26
+ # @raise ArgumentError if the storage instance or the `Site` or `Node`
27
+ # model cannot be determined
28
+ # @overload set_instance(instance)
29
+ # @param [Circuit::Storage::Nodes::BaseStore,Circuit::Storage::Sites::BaseStore]
30
+ # instance storage instance
31
+ # @return [Circuit::Storage::Nodes::BaseStore,Circuit::Storage::Sites::BaseStore]
32
+ # storage instance
33
+ # @overload set_instance(klass_or_name, *args)
34
+ # @param [Class,String,Symbol] klass_or_name class or name of class
35
+ # (under Circuit::Storage::Nodes or
36
+ # Circuit::Storage::Sites) for storage
37
+ # instance
38
+ # @param [Array] args any arguments to instantiate the storage instance
39
+ # @return [Circuit::Storage::Nodes::BaseStore,Circuit::Storage::Sites::BaseStore]
40
+ # storage instance
41
+ def set_instance(*args)
42
+ klass = nil
43
+
44
+ case args.first
45
+ when Nodes::BaseStore, Sites::BaseStore
46
+ @instance = args.first
47
+ when Class
48
+ klass = args.first
49
+ when String, Symbol
50
+ # TODO do we need to fall up the module hiearchy to find the constant?
51
+ # (e.g. a store defined in the Kernel namespace?)
52
+ klass = const_get(args.first.to_s.camelize)
53
+ else
54
+ raise ArgumentError, "Unexpected type for storage instance: %s"%[args.first.class]
55
+ end
56
+
57
+ if klass
58
+ @instance = klass.new(*args[1..-1])
59
+ end
60
+
61
+ case @instance
62
+ when Nodes::BaseStore
63
+ ::Circuit.const_set(:Node, @instance.class.const_get(:Node))
64
+ when Sites::BaseStore
65
+ ::Circuit.const_set(:Site, @instance.class.const_get(:Site))
66
+ else
67
+ bad_instance = @instance; @instance = nil
68
+ raise ArgumentError, "Cannot determine a Site or Node class for storage type: %s"%[bad_instance.class]
69
+ end
70
+
71
+ @instance
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,70 @@
1
+ require 'active_support/concern'
2
+ require 'active_model/naming'
3
+ require 'active_model/attribute_methods'
4
+
5
+ module Circuit
6
+ module Storage
7
+ module MemoryModel
8
+ extend ActiveSupport::Concern
9
+
10
+ include ActiveModel::AttributeMethods
11
+ include Compatibility::ActiveModel31
12
+ attr_reader :attributes, :errors
13
+ attr_accessor :name
14
+
15
+ included do
16
+ extend ActiveModel::Naming
17
+ class_attribute :all
18
+ self.all = Array.new
19
+ end
20
+
21
+ module ClassMethods
22
+ def setup_attributes(*attrs)
23
+ attribute_method_suffix '?'
24
+ attribute_method_suffix '='
25
+ define_attribute_methods attrs.collect(&:to_sym)
26
+ end
27
+ end
28
+
29
+ def attributes=(hash)
30
+ @attributes = hash.with_indifferent_access
31
+ end
32
+
33
+ def save!
34
+ self.save ? true : raise("Invalid %s: %p"%[self.class, self.errors])
35
+ end
36
+
37
+ def persisted?
38
+ @persisted
39
+ end
40
+
41
+ def persisted!(val=true)
42
+ @persisted = val
43
+ end
44
+
45
+ def eql?(obj)
46
+ obj.instance_of?(self.class) && obj.attributes == self.attributes
47
+ end
48
+
49
+ protected
50
+
51
+ def attribute(key)
52
+ attributes[key]
53
+ end
54
+
55
+ def attribute=(key, val)
56
+ attributes[key] = val
57
+ end
58
+
59
+ def attribute?(attr)
60
+ !attributes[attr.to_sym].blank?
61
+ end
62
+
63
+ def memory_model_setup
64
+ @persisted = false
65
+ @attributes = HashWithIndifferentAccess.new
66
+ @errors = ActiveModel::Errors.new(self)
67
+ end
68
+ end
69
+ end
70
+ end