middlegem 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d8e1dd451fcba2d15305534945c4d6fe88c7f6c9fb29c8e8059241503291432b
4
+ data.tar.gz: 61fcd25df93bb9902e6a85815ab6b63696c6c2f6d753cceeac4afa521143ca12
5
+ SHA512:
6
+ metadata.gz: 95319c1cc7be36e04290c0a30e3494834dea8b9aef142ca1c44582a80ba9037a9325efb7a764dff32c1483e591c62ca14794d07c4c810d29dcf171c6e40eb171
7
+ data.tar.gz: 5790037f20f4c2a7b88f894c74377bd1ca75031ebc87c4c3fc601e3d4b9de21436f11a5aeb7f28b908da0462ef8962d9cfadbfbfdf491781faee122141a74ec8
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ # Default
2
+
3
+ /.bundle/
4
+ /.yardoc
5
+ /_yardoc/
6
+ /coverage/
7
+ /doc/
8
+ /pkg/
9
+ /spec/reports/
10
+ /tmp/
11
+
12
+ # rspec failure tracking
13
+ .rspec_status
14
+
15
+ # Ignore Gemfile.lock, since this is a gem.
16
+
17
+ Gemfile.lock
18
+
19
+ # Ignore byebug history
20
+
21
+ .byebug_history
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,11 @@
1
+ AllCops:
2
+ NewCops: enable
3
+ Layout/LineLength:
4
+ Max: 120
5
+ Style/GuardClause:
6
+ Enabled: false
7
+ Metrics/BlockLength:
8
+ Exclude:
9
+ - 'spec/**/*.rb'
10
+ require:
11
+ - rubocop-rspec
data/.travis.yml ADDED
@@ -0,0 +1,16 @@
1
+ env:
2
+ global:
3
+ - CC_TEST_REPORTER_ID="c5db72d9e061605b89ff87aa3914c44362c4a45efdcb5cd75d26d90b10ec004c"
4
+ language: ruby
5
+ rvm:
6
+ - 2.5
7
+ before_script:
8
+ - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
9
+ - chmod +x ./cc-test-reporter
10
+ - ./cc-test-reporter before-build
11
+ script:
12
+ - bundle exec rubocop -DESP
13
+ - bundle exec rake spec
14
+ after_script:
15
+ - ./cc-test-reporter format-coverage -t simplecov
16
+ - ./cc-test-reporter upload-coverage
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --private --protected
data/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0] - 2021-07-08
10
+ ### Added
11
+ - The initial `middlegem` gem.
12
+
13
+ [Unreleased]: https://github.com/jacoblockard99/middlegem/compare/v0.1.0...HEAD
14
+ [0.1.0]: https://github.com/jacoblockard99/middlegem/releases/tag/v0.1.0
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in middlegem.gemspec
6
+ gemspec
7
+
8
+ gem 'rake', '~> 13.0'
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 Jacob
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,199 @@
1
+ # Middlegem
2
+
3
+ [![Build Status](https://travis-ci.com/jacoblockard99/middlegem.svg?branch=master)](https://travis-ci.com/jacoblockard99/middlegem)
4
+ [![Inline docs](http://inch-ci.org/github/jacoblockard99/middlegem.svg?branch=master)](http://inch-ci.org/github/jacoblockard99/middlegem)
5
+ [![Maintainability](https://api.codeclimate.com/v1/badges/b43ed85211cb562678bb/maintainability)](https://codeclimate.com/github/jacoblockard99/middlegem/maintainability)
6
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/b43ed85211cb562678bb/test_coverage)](https://codeclimate.com/github/jacoblockard99/middlegem/test_coverage)
7
+
8
+ `middlegem` is a Ruby gem that provides one-way middleware chain functionality. It aims to be simple and reliable. It might be a good fit for you if:
9
+ - **You want simplicity and reliability.**
10
+ - **You don't need two-way middleware.** `middlegem` does not allow processing both the "request" and the "response", for example. For that kind of functionality, I would recommend checking out [ruby-middleware](https://github.com/Ibsciss/ruby-middleware).
11
+ - **You want to explicitly define the order of your middlwares.** `middlegem` encourages you to explicitly define your middlewares and the order they should run.
12
+
13
+ ## Links
14
+
15
+ - [API Docs](https://rdoc.info/github/jacoblockard99/middlegem)
16
+ - [CHANGELOG.md](CHANGELOG.md)
17
+ - [Releases](https://github.com/jacoblockard99/middlegem/releases)
18
+
19
+ ## Installation
20
+
21
+ `middlegem` is a Ruby gem. If you use Bundler, you may install it by adding it to your `Gemfile`, like so:
22
+
23
+ ```ruby
24
+ gem 'middlegem'
25
+ ```
26
+
27
+ And then execute:
28
+
29
+ $ bundle install
30
+
31
+ Or you may install it manually with:
32
+
33
+ $ gem install middlegem
34
+
35
+ `middlegem` has zero dependencies and requires very little setup to get started!
36
+
37
+ ## Key Concepts
38
+
39
+ `middlegem` is broken into three key parts: middlewares, middleware definitions, and middleware stacks.
40
+
41
+ **Middlewares** are the heart and soul of the gem. In essence, a middleware is a single transforming function that accepts input and produces output. In `middlegem`, any object that responds to the `call` method can be a middleware. By convention, however, middleware classes derive from `Middlegem::Middleware`.
42
+
43
+ Middlewares in `middlegem` are designed to operate on *argument lists*. This has two consequences:
44
+ 1. A middleware's `call` method should simply accept the arguments it expects to transform—no need to accept and "arguments array" and try to parse it!
45
+ 2. The `call` method **must** return an array. Because it is transforming an argument list, it must also return an argument list, i.e. an array.
46
+
47
+ While you can certainly use `middlegem` with a single input, always remember to return an array in your middleware `call` methods.
48
+
49
+ **Midleware definitions** are a key difference in `middlegem` from other middleware gems. They strive to solve a common problem with middlewares. Imagine, for example, that you have two middlewares: one that converts an input string to an integer, and another that multiplies that number by 10. Obviously, the conversion middleware must run first, or an error will occur. With a simple example like this, it is trivial to simply insert the middlewares in the right place at the right time. But as you begin adding more middlewares and—worse—begin allowing *custom* middlewares to be defined, things quickly become unmanageable! It becomes impossible to know exactly where a given middleware should be inserted in a middleware stack.
50
+
51
+ This is where middleware definitions come in. A middleware definition is essentially an object that determines 1) what middlewares are permitted in a middleware stack, and 2) in what order those middlewares should be run. Any object that implements a `defined?` method and a `sort` method can be avalid middleware definition, though by convention middleware definitions derive from `Middlegem::Definition`. The only middleware definition that currently ships with `middlegem` is `Middlegem::ArrayDefinition`, which allows you to define an ordered list of permitted middleware classes.
52
+
53
+ Finally, **middleware stacks**, represented by `Middlegem::Stack`, are chains of middlewares. Every `Middlegem::Stack` has a single middleware definition that determines how to run its middlewares. Note that `Middlegem::Stack` has no fancy methods for inserting middlewares at specific locations—it relies on Ruby's built-in methods. Instead, it allows ordering to be determined by the middleware definition.
54
+
55
+ ## Usage
56
+
57
+ ### Basic Usage
58
+
59
+ The easiest way to define middlewares is to create a class with a `call` method that optionally extends `Middlegem::Middleware`. In this example and the following ones, assume that these middlewares are defined:
60
+
61
+ ```ruby
62
+ class ParenthesesMiddleware << Middlegem::Middleware
63
+ def call(input)
64
+ ["(#{input})"]
65
+ end
66
+ end
67
+
68
+ class BracketsMiddleware << Middlegem::Middleware
69
+ def call(input)
70
+ ["[#{input}]"]
71
+ end
72
+ end
73
+
74
+ class BracesMiddleware << Middlegem::Middleware
75
+ def call(input)
76
+ ["{#{input}}"]
77
+ end
78
+ end
79
+
80
+ class MultiplierMiddleware << Middlegem::Middleware
81
+ attr_accessor :multiplier
82
+
83
+ def initialize(multiplier)
84
+ @multiplier = multiplier
85
+ end
86
+
87
+ def call(num)
88
+ [num * multiplier]
89
+ end
90
+ end
91
+ ```
92
+
93
+ Now you'll need to _define_ your middleware. If you're using Rails, initializers are usually a good place to do this. The easiest way to create a middleware definition is using `Middlegem::ArrayDefinition`, which allows you to specify an array of middleware classes. For example:
94
+
95
+ ```ruby
96
+ DEFINITION = Middlegem::ArrayDefinition.new([
97
+ MultiplierMiddleware,
98
+ ParenthesesMiddleware,
99
+ BracketsMiddleware,
100
+ BracesMiddleware
101
+ ])
102
+ ```
103
+
104
+ Notice that the `MultiplierMiddleware` is at the top, because it must be given a number, and the others are arranged in "mathematical" order. Now, we can create a middleware stack with our definition.
105
+
106
+ ```ruby
107
+ stack = Middlegem::Stack.new(DEFINITION)
108
+ ```
109
+
110
+ And add some middlewares, however you like:
111
+
112
+ ```ruby
113
+ stack.middlewares = [BracketsMiddleware]
114
+ stack.middlewares += [MultiplierMiddleware, BracesMiddleware]
115
+ stack.middlewares << ParenthesesMiddleware
116
+ ```
117
+
118
+ Finally, we can call the stack with a number:
119
+
120
+ ```ruby
121
+ stack.call(10) # => ["{[(100)]}"]
122
+ ```
123
+
124
+ Notice how the number is first multiplied, then given parentheses, then given brackets, then given braces, exactly as specified in the middleware definition.
125
+
126
+ ### Tie Resolvers
127
+
128
+ You may have noticed a problem here. What if multiple middleware instances of the same type are added to a stack. How will it know which to call? Take this code, for example, where procs are used as middleware:
129
+
130
+ ```ruby
131
+ DEFINITION = Middlegem::ArrayDefinition.new([Proc])
132
+
133
+ to_int = proc { |s| Integer(s) }
134
+ square = proc { |n| n*n }
135
+
136
+ stack = Middlegem::Stack.new(DEFINITION, middlewares: [
137
+ square,
138
+ to_int
139
+ ])
140
+ ```
141
+
142
+ If `stack.call('5')` were run right now, the program would try to square `'5'`, *then* convert it to an integer. Moreover, there is no way to specify which should come first—they are both procs, after all. For this reason, it is recommended that you keep all your middlewares in separate classes, so they can be defined easily. There are two potential solutions, however.
143
+
144
+ First, `ArrayDefinition.new` accepts an optional "tie resolver" that will be called in such cases. For example, let's say we have this middleware:
145
+
146
+ ```ruby
147
+ class AppendMiddleware
148
+ attr_accessor :appended
149
+
150
+ def initialize(appended)
151
+ @appended = appended
152
+ end
153
+
154
+ def call(input)
155
+ [input + appended]
156
+ end
157
+ end
158
+ ```
159
+
160
+ Obviously, the order of even individual `AppendMiddleware`s matters. "TAB" is a very differnt word from "BAT"! Imagining that we want the letters to be alphabetized, here is one potential solution:
161
+
162
+ ```ruby
163
+ DEFINITION = Middlegem::ArrayDefinition.new([AppendMiddleware], resolver: ->(ties) {
164
+ if ties.count > 1 && ties.first.is_a? AppendMiddleware
165
+ return ties.sort_by(&:appended)
166
+ end
167
+ ties
168
+ })
169
+
170
+ stack = Middlegem::Stack.new(DEFINITION, middlewares: [
171
+ AppendMiddleware.new('B'),
172
+ AppendMiddleware.new('A'),
173
+ AppendMiddleware.new('C'),
174
+ AppendMiddleware.new('E'),
175
+ AppendMiddleware.new('D')
176
+ end
177
+
178
+ stack.call('') # => ['ABCDE']
179
+ ```
180
+
181
+ As you can see, the resolver passed to `ArrayDefinition.new` will be called with an array of middleware whenever multiple middleware with the same class are encountered. The resolver is then expected to sort and return the array appropriately. In this case, we simply check whether the tied middlewares are `AppendMiddleware`s and sort them by their `appended` attribute if so.
182
+
183
+ While this works, the limitations quickly become obvious. Mainly, it requires a bunch of branching `else/if` or `case/when` structures in the resolver since that one resolver is called for all ties. While it may work for very simple use cases (such as preventing multiple instances of the same middleware at all), it is not feasible for anything more complicated.
184
+
185
+ For more complicated scenarios, it is instead recommended that you create your own implementation of `Middlegem::Definition` that allows ordering the middlewares in some other way. Perhaps you could set "priorities" on the middlewares, or organize them into "groups"—the possibilities with this method are limitless!
186
+
187
+ ## Development
188
+
189
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
190
+
191
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
192
+
193
+ ## Contributing
194
+
195
+ Bug reports and pull requests are welcome on GitHub at https://github.com/jacoblockard99/middlegem.
196
+
197
+ ## License
198
+
199
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require 'rubocop/rake_task'
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'middlegem'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require 'pry'
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/lib/middlegem.rb ADDED
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'middlegem/version'
4
+ require_relative 'middlegem/middleware'
5
+ require_relative 'middlegem/definition'
6
+ require_relative 'middlegem/stack'
7
+ require_relative 'middlegem/array_definition'
8
+
9
+ # {Middlegem} is a namespace that contains all modules in the +middlegem+ gem.
10
+ #
11
+ # @author Jacob Lockard
12
+ # @since 0.1.0
13
+ module Middlegem
14
+ # {Error} is a subclass of {https://ruby-doc.org/core-2.5.0/StandardError.html StandardError}
15
+ # from which all custom errors in +middlegem+ are derived. One potential use for this class is
16
+ # to rescue all custom errors produced by +middlegem+. For example:
17
+ #
18
+ # begin
19
+ # # Do something risky with middlegem here...
20
+ # rescue Middlegem::Error
21
+ # # Catch any middlegem-specific error here...
22
+ # end
23
+ #
24
+ # @see https://ruby-doc.org/core-2.0.0/Exception.html
25
+ class Error < StandardError; end
26
+ end
27
+
28
+ require_relative 'middlegem/invalid_middleware_error'
29
+ require_relative 'middlegem/unpermitted_middleware_error'
30
+ require_relative 'middlegem/invalid_definition_error'
31
+ require_relative 'middlegem/invalid_middleware_output_error'
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Middlegem
4
+ # {ArrayDefinition} is an implementation of {Definition} that allows middlewares to be
5
+ # explicitly defined and ordered by class in an array. A basic example of usage is:
6
+ #
7
+ # definition = Middlegem::ArrayDefinition.new([
8
+ # MiddlewareOne, # appends '1'
9
+ # MiddlewareTwo, # appends '2'
10
+ # MiddlewareThree, # appends '3'
11
+ # MiddlewareFinal # appends '.'
12
+ # ])
13
+ #
14
+ # stack = Middlegem::Stack.new(definition, middlewares: [
15
+ # MiddlewareThree.new,
16
+ # MiddlewareFinal.new,
17
+ # MiddlewareOne.new,
18
+ # MiddlewareTwo.new
19
+ # ])
20
+ #
21
+ # stack.call('hello') # => ['hello123.']
22
+ #
23
+ # Notice that the middlewares are called in the order they are specified in the definition
24
+ # array.
25
+ #
26
+ # If two or more middlewares are encountered that have the same class, they will be left in the
27
+ # order they were added. This behavior can be overriden by setting a tie resolver. The
28
+ # following code, for example, raises an error when multiple +MiddlewareFinal+ middlewares
29
+ # are encountered:
30
+ #
31
+ # middlewares = [
32
+ # MiddlewareOne,
33
+ # MiddlewareTwo,
34
+ # MiddlewareThree,
35
+ # MiddlewareFinal
36
+ # ]
37
+ #
38
+ # tie_resolver = proc do |ties|
39
+ # raise "Can't run multiple MiddlewareFinals!" if ties.first.is_a? MiddlewareFinal
40
+ # ties
41
+ # end
42
+ #
43
+ # definition = Middlegem::ArrayDefinition.new(middlewares, resolver: tie_resolver)
44
+ #
45
+ # stack = Middlegem::Stack.new(definition, middlewares: [
46
+ # MiddlewareTwo.new,
47
+ # MiddlewareOne.new,
48
+ # MiddlewareFinal.new,
49
+ # MiddlewareThree.new,
50
+ # MiddlewareFinal.new
51
+ # ])
52
+ #
53
+ # stack.call('hello') # => RuntimeError (Can't run multiple MiddlewareFinals!)
54
+ #
55
+ # When the two +MiddlewareFinal+ instances are encountered, the tie resolver is run, which
56
+ # raises the error.
57
+ #
58
+ # Of course, this is only scratching the surface of what is possible with
59
+ # a custom tie resolver. You might, for example, simply skip other instances of
60
+ # +MiddlewareFinal+, rather than raising an error. A word of caution is in order, however!
61
+ # It is not recommended to try anything too complicated with the tie resolver because it is run
62
+ # for <em>all ties whatsoever</em>. That means that, while you could technically try to sort
63
+ # middlewares with the same class based on some other factor---there is even an example
64
+ # in +spec/middlegem/array_definition_spec.rb+---it potentially results in long +if/else+ or
65
+ # +case/when+ constructions because each type must be dealt with separately. Use at your own risk!
66
+ #
67
+ # In general, if you need to use a tie resolver for anything but the most basic of tasks, you
68
+ # should probably just create your own {Definition} implementation with the required
69
+ # functionality. {ArrayDefinition} is intended primarily for defining middlewares according to
70
+ # their classes and nothing more.
71
+ #
72
+ # @author Jacob Lockard
73
+ # @since 0.1.0
74
+ # @see Definition
75
+ class ArrayDefinition < Definition
76
+ # An array of the middleware classes defined by this {ArrayDefinition}. Middlewares will only
77
+ # be permitted if their class is in this array will be run in the order specified here.
78
+ # @return [Array<Class>] the array of defined classes.
79
+ attr_accessor :defined_classes
80
+
81
+ # The callable object to use to break ties when sorting middlewares. When multiple
82
+ # middlewares of the same type are encountered, this object will be called with an
83
+ # array of all tied middlewares. The resolver should sort and return the array as
84
+ # appropriate.
85
+ # @return [#call] the middleware tie resolver.
86
+ attr_reader :resolver
87
+
88
+ # Creates a new instance of {ArrayDefinition} with the given array of defined classes and,
89
+ # optionally, a custom tie resolver.
90
+ # @param defined_classes [Object<Class>] an ordered array of classes to be defined by this
91
+ # {ArrayDefinition} (see {#defined_classes}).
92
+ # @param resolver [#call, nil] a callable object to use when middlewares of the same class
93
+ # are encountered (see {#resolver}). If a +nil+ resolver is passed (the default), the
94
+ # default resolver will be used, which keeps tied middlewares in the order they are passed
95
+ # to {#sort}.
96
+ def initialize(defined_classes, resolver: nil)
97
+ resolver = ->(*ties) { ties } if resolver.nil?
98
+
99
+ @defined_classes = defined_classes
100
+ @resolver = resolver
101
+
102
+ super()
103
+ end
104
+
105
+ # Determines whether the given middleware is defined according to this {ArrayDefinition} by
106
+ # checking whether its class is contained in the list of defined classes
107
+ # (i.e. {#defined_classes}).
108
+ # @param middleware [Object] the middleware to check.
109
+ # @return [bool] whether the middleware is defined.
110
+ def defined?(middleware)
111
+ defined_classes.include?(middleware.class)
112
+ end
113
+
114
+ # Sorts the given array of middlewares according to this {ArrayDefinition}. Middlewares are
115
+ # sorted according to the order in which their classes are specified in {#defined_classes}.
116
+ # If multiple middlewares of the same type are encountered, they will be resolved with the
117
+ # {#resolver}.
118
+ def sort(middlewares)
119
+ defined_classes.map { |c| resolver.call(matches(middlewares, c)) }.flatten
120
+ end
121
+
122
+ private
123
+
124
+ # Gets all the middlewares in the given array whose class is the given class.
125
+ # @param middlewares [Array<Object>] the array of middlewares to search.
126
+ # @param klass [Class] the class to search for.
127
+ # @return [Array<Object>] the matched middlewares.
128
+ def matches(middlewares, klass)
129
+ middlewares.select { |m| m.instance_of?(klass) }
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Middlegem
4
+ # {Definition} is an abstract class whose implementations are capable of defining what
5
+ # middlewares are permitted in a given context and in what order they should be executed. Note
6
+ # that this concept of "middleware definitions" is a major difference from other middleware
7
+ # solutions, where middlewares are simply inserted into a stack. In +middlegem+, a
8
+ # {Definition} determines what order the middleware in a {Stack} should be called. This greatly
9
+ # decreases the likelihood of "middleware conflicts", where one middleware expects another to
10
+ # have already run. The downside, of course, is the verbosity---all middleware must be defined.
11
+ #
12
+ # It should be noted, however, that the concept of a "middleware definition" is completely
13
+ # flexible. If you would prefer to simply insert middlewares into a stack without defining
14
+ # them, you can create a {Definition} implentation whose {#sort} method just returns the
15
+ # unsorted middlewares. And if you want to allow any middlewares to be inserted, simply return
16
+ # +true+ from the {#defined?} method. On the other hand, there is a lot of flexibility in how
17
+ # you determine the middleware order: define a DSL, let middleware have dependencies, define
18
+ # middleware "groups", allow middleware "priorities"---the possibilities are endless!
19
+ #
20
+ # Currently, the only default implementation is {ArrayDefinition}, which executes
21
+ # middleware based on an ordered list of middleware classes.
22
+ #
23
+ # Finally, you might notice that {Definition} contains no actual instance method
24
+ # implementations. In other words, for all intents and pruposes, it is empty! This is
25
+ # intentional. A middleware definition is *any* object that implements both a {#defined?} and a
26
+ # {#sort} method (see {.valid?}). You may extend this class, however, to explicitly mark your
27
+ # middleware definition classes as middleware definitions.
28
+ #
29
+ # @author Jacob Lockard
30
+ # @since 0.1.0
31
+ # @abstract
32
+ class Definition
33
+ # Determines whether the given object is a valid middleware definition. Currently, any object
34
+ # that implements a +defined?+ method and a +sort+ method is valid.
35
+ #
36
+ # @param definition [Object] the middleware definition to check.
37
+ # @return [bool] whether the given object is a valid middleware definition.
38
+ def self.valid?(definition)
39
+ definition.respond_to?(:defined?) && definition.respond_to?(:sort)
40
+ end
41
+
42
+ # @!method defined?(middleware)
43
+ # Should determine whether the given middleware is defined according to this
44
+ # {Definition}. Feel free to determine this however you like! This method will
45
+ # be used to validate middlewares added to a {Stack}.
46
+ # @param middleware [Object] the middleware object to check.
47
+ # @return [bool] whether the given middleware object is defined.
48
+
49
+ # @!method sort(middlewares)
50
+ # Should sort the given array of middlewares according to this {Definition}. In a {Stack},
51
+ # middlewares will be called in the order returned.
52
+ # @param middlewares [Array<Object>] the middlewares to sort.
53
+ # @return [Array<Object>] the sorted middlewares.
54
+ end
55
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Middlegem
4
+ # An error that is raised when an object that is not a valid middleware definition is used like
5
+ # one.
6
+ #
7
+ # @author Jacob Lockard
8
+ # @since 0.1.0
9
+ class InvalidDefinitionError < Error; end
10
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Middlegem
4
+ # An error that is raised when an object that is not a valid middleware is used like one.
5
+ #
6
+ # @author Jacob Lockard
7
+ # @since 0.1.0
8
+ class InvalidMiddlewareError < Error; end
9
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Middlegem
4
+ # An error that is raised when a middleware object returns an invalid output. This error is
5
+ # most commonly raised when a middleware does not return an array.
6
+ #
7
+ # @author Jacob Lockard
8
+ # @since 0.1.0
9
+ class InvalidMiddlewareOutputError < Error; end
10
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Middlegem
4
+ # {Middleware} is an abstract representation of a single "middleware". A middleware is a
5
+ # transforming function that accepts arbitrary input and produces arbitrary output.
6
+ # Middlewares can be chained with {Stack} to produce powerful, flexible data-transforming
7
+ # layers.
8
+ #
9
+ # One important concept to note is that middlewares in +middlegem+ are "one-way". In other
10
+ # words, they cannot transform both a "request" and a "response". For this functionality,
11
+ # please see other gems such as {https://github.com/Ibsciss/ruby-middleware ruby-middleware} or
12
+ # (for web requests) {https://github.com/rack/rack rack}.
13
+ #
14
+ # Middlewares in +middlegem+ are also slightly different in exactly what they operate upon.
15
+ # Whereas the middlewares in +ruby-middleware+ simply transform a single +env+
16
+ # variable, +middlegem+ middlewares transform an entire argument list. This may or may not be
17
+ # desirable, as it requires all middlewares to return an array, but it highlights the primary
18
+ # use case for +middlegem+. While it can be used for a variety of purposes, +middlegem+ was
19
+ # specifically designed for filtering and changing arguments passed to a method.
20
+ #
21
+ # Finally, you might notice that {Middleware} contains no actual instance method
22
+ # implementations. In other words, for all intents and pruposes, it is empty! This is
23
+ # intentional. A middleware is *any* object that implements a {#call} method (you read that
24
+ # right---a proc is a valid middleware!). You may extend this class, however, to explicitly
25
+ # mark your middleware classes as middlewares.
26
+ #
27
+ # @author Jacob Lockard
28
+ # @since 0.1.0
29
+ # @abstract
30
+ # @see Middlegem::Stack
31
+ class Middleware
32
+ # Determines whether the given object is a valid middleware. Currently, any object that
33
+ # responds to the +call+ method is valid.
34
+ #
35
+ # @param middleware [Object] the middleware to check.
36
+ # @return [bool] whether the given object is a valid middleware.
37
+ def self.valid?(middleware)
38
+ middleware.respond_to?(:call)
39
+ end
40
+
41
+ # @!method call(*args)
42
+ # The method called to actually execute the middleware. It is passed the output of the
43
+ # previous middleware in the chain and should return the appropriately transformed output
44
+ # to be passed to the next middleware. Note that the splat operator is intentional! This
45
+ # method will be called with actual arguments, not an array. This means that you can
46
+ # simply define the arguments you expect directly in the method signature, rather than
47
+ # accepting a splatted array and accessing its elements. For example, the following is
48
+ # possible:
49
+ # class MyMiddleware
50
+ # def call(first, last, email)
51
+ # return "#{email} <#{first} #{last}>"
52
+ # end
53
+ # end
54
+ # @param args [Array<Object>] the input to be transformed, usually the output of the
55
+ # previous middleware in a middleware chain.
56
+ # @return [Object, Array<Object>] the transformed output. Note that if an array is
57
+ # returned, it will be splatted before being passed to the next middleware.
58
+ # @note This method must be implemented by all middleware!
59
+ end
60
+ end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Middlegem
4
+ # {Stack} is a class that represents a chain of middlewares, which, when called, can
5
+ # arbitrarily transform a given input. Most of the functionality provided by +middlegem+ lies
6
+ # in this class. Using {Stack} is simple: create a new instance with the desired definition,
7
+ # add whatever middlewares you want to use, and {#call} it.
8
+ #
9
+ # A very basic example of usage is:
10
+ #
11
+ # class LastNameMiddleware < Middlegem::Middleware
12
+ # def call(name)
13
+ # "The Honorable #{name}"
14
+ # end
15
+ # end
16
+ #
17
+ # class EmailStringMiddleware < Middlegem::Middleware
18
+ # def initialize(email)
19
+ # @email = email
20
+ # end
21
+ #
22
+ # def call(name)
23
+ # "#{@email} <#{name}>"
24
+ # end
25
+ # end
26
+ #
27
+ # definitions = [
28
+ # LastNameMiddleware,
29
+ # EmailStringMiddleware
30
+ # ]
31
+ #
32
+ # stack = Middlegem::Stack.new(Middlegem::ArrayDefinition.new(definitions))
33
+ # stack.middlewares += [EmailStringMiddleware.new('mail@test.com'), LastNameMiddleware.new]
34
+ #
35
+ # stack.call('Jacob') # => "mail@test.com <The Honorable Jacob>"
36
+ #
37
+ # Notice that, even though the +EmailStringMiddleware+ was added before the
38
+ # +LastNameMiddleware+, the +LastNameMiddleware+ was still run first since it was defined
39
+ # first. That is a core principle of +middlegem+---rather than providing extensive methods to
40
+ # insert middleware in a specific place along the chain, +middlegem+ allows you to define
41
+ # the order explicitly. Also note that there are a variety of ways that you could specify the
42
+ # middleware order by extending {Definition}.
43
+ #
44
+ # @author Jacob Lockard
45
+ # @since 0.1.0
46
+ # @see Middleware
47
+ # @see Definition
48
+ # @see ArrayDefinition
49
+ class Stack
50
+ # An array containing the middlewares represented by this {Stack}. You can insert middlewares
51
+ # in any way you like by accessing this attribute directly and using ruby's built-in array
52
+ # methods. If desired, you can even assign a new array to it. All middlewares will be
53
+ # validated before being run. To be run, a middleware must be *valid* as defined by
54
+ # {Middleware.valid?} and it must be *defined* according to the {Definition} instance
55
+ # in {#definition}.
56
+ # @return [Array<Object>] the middlewares contained in this stack.
57
+ attr_accessor :middlewares
58
+
59
+ # The {Definition} used to determine what middlewares are permitted in this stack
60
+ # and in what order they should be run. Note that this attribute may be any object that is a
61
+ # valid definition according to {Definition.valid?}.
62
+ # @return [Definition] the middleware definition of this middleware stack.
63
+ attr_reader :definition
64
+
65
+ # Creates a new instance of {Stack} with the given middleware definition and,
66
+ # optionally, an array of middlewares. Note that middlewares will be validated, not
67
+ # immediately, but before being run.
68
+ # @param definition [Definition] the middleware definition to use to determine
69
+ # what middleware to permit in this stack and in what order to run them. May be any object
70
+ # that is a valid definition according to {Definition.valid?}.
71
+ # @param middlwares [Array<Object>] an optional array of initial middlewares in this stack.
72
+ def initialize(definition, middlewares: [])
73
+ unless Definition.valid?(definition)
74
+ raise InvalidDefinitionError, "The middleware definition #{definition} is invalid!"
75
+ end
76
+
77
+ @definition = definition
78
+ @middlewares = middlewares
79
+ end
80
+
81
+ # Transforms the given input by calling all the middlewares in this stack, as defined by the
82
+ # middleware {#definition}. Note that, as mentioned in {Middleware}, middlewares in
83
+ # +middlegem+ transform argument lists. Thus, the arguments are already splatted---there is
84
+ # no need to pass a single array of arguments as the only parameter, unless you actually want
85
+ # to transform just a single array. Also, middleware *must* return an array of arguments,
86
+ # which will be splatted when passed to {Middleware#call}.
87
+ #
88
+ # Midlewares are validated before being run or sorted. If a middleware is encountered that
89
+ # is either invalid or unpermitted, an appropriate error will be raised.
90
+ #
91
+ # @param args [Array<Object>] the array of input arguments.
92
+ # @return [Array<Object>] the output of the last middleware in the chain.
93
+ # @raise [InvalidMiddlewareError] when one of the middlewares in {#middlewares} is not valid,
94
+ # as defined by {Middleware.valid?}.
95
+ # @raise [UnpermittedMiddlewareError] when one of the middlewares in {#middlewares} has not
96
+ # been defined, and is thus not permitted, according to the {Definition} instance in
97
+ # {#definition}.
98
+ def call(*args)
99
+ # Validate the middlewares.
100
+ middlewares.each { |m| ensure_valid!(m) }
101
+
102
+ # Sort the middlewares.
103
+ sorted = definition.sort(middlewares)
104
+
105
+ # Run each middleware with the output of the previous one, ensuring that each output is
106
+ # valid. For the first middleware, use `args` as the input.
107
+ last_output = args
108
+ sorted.each do |middleware|
109
+ last_output = middleware.call(*last_output)
110
+ ensure_valid_output!(middleware, last_output)
111
+ end
112
+
113
+ last_output
114
+ end
115
+
116
+ private
117
+
118
+ # Ensures that the given middleware is a valid middleware for this middleware stack, raising
119
+ # an appropriate error if not. A middleware is valid if:
120
+ # 1. it is valid according to {Middleware.valid?}, and
121
+ # 2. it is defined according to {#definition}.
122
+ # @param middleware [Object] the middleware to validate.
123
+ # @return [void]
124
+ def ensure_valid!(middleware)
125
+ unless Middleware.valid?(middleware)
126
+ raise InvalidMiddlewareError, "The middleware #{middleware} is not a valid middleware!"
127
+ end
128
+
129
+ unless definition.defined?(middleware)
130
+ raise UnpermittedMiddlewareError, "The middleware #{middleware} has not been defined!"
131
+ end
132
+ end
133
+
134
+ # Ensures that the given output of the given middleware is valid for all the intents and
135
+ # purposes of this stack. Essentially, the output is valid if it can be passed, splatted, to
136
+ # the next middleware, or returned at the end of the stack. Currently, this method only
137
+ # checks whether the output is an "array" (as defined by the splat operator). If the output
138
+ # is invalid, an appropriate error will be raised.
139
+ # @param middleware [Object] the middleware whose output is being validated. This object is
140
+ # only used to generate appropriate error messages.
141
+ # @param output [Object] the middleware output to validate.
142
+ # @return [void]
143
+ def ensure_valid_output!(middleware, output)
144
+ unless output == [*output]
145
+ raise InvalidMiddlewareOutputError, <<~ERR
146
+ The middleware #{middleware} outputted #{output}, which is not an array!
147
+ ERR
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Middlegem
4
+ # An error that is raised when a middleware that is not permitted in a given context is used.
5
+ #
6
+ # @author Jacob Lockard
7
+ # @since 0.1.0
8
+ class UnpermittedMiddlewareError < Error; end
9
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Middlegem
4
+ VERSION = '0.1.0'
5
+ end
data/middlegem.gemspec ADDED
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/middlegem/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'middlegem'
7
+ spec.version = Middlegem::VERSION
8
+ spec.authors = ['Jacob']
9
+ spec.email = ['jacoblockard99@gmail.com']
10
+
11
+ spec.summary = 'Simple one-way middleware.'
12
+ spec.description = <<~DESC
13
+ Middlegem is a ruby gem that provides simple middleware chains with goals of simplicity and
14
+ reliability.
15
+ DESC
16
+ spec.homepage = 'https://github.com/jacoblockard99/middlegem'
17
+ spec.license = 'MIT'
18
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.5.0')
19
+
20
+ spec.metadata['homepage_uri'] = spec.homepage
21
+ spec.metadata['source_code_uri'] = 'https://github.com/jacoblockard99/middlegem'
22
+ spec.metadata['changelog_uri'] = 'https://github.com/jacoblockard99/middlegem/CHANGELOG.md'
23
+ # Specify which files should be added to the gem when it is released.
24
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
26
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
27
+ end
28
+ spec.bindir = 'exe'
29
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
30
+ spec.require_paths = ['lib']
31
+
32
+ spec.add_development_dependency 'rspec', '~> 3.0'
33
+ spec.add_development_dependency 'rubocop', '~> 1.18'
34
+ spec.add_development_dependency 'rubocop-rspec', '~> 2.4'
35
+ spec.add_development_dependency 'simplecov', '0.17'
36
+
37
+ # Uncomment to register a new dependency of your gem
38
+ # spec.add_dependency "example-gem", "~> 1.0"
39
+
40
+ # For more information and examples about making a new gem, checkout our
41
+ # guide at: https://bundler.io/guides/creating_gem.html
42
+ end
metadata ADDED
@@ -0,0 +1,127 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: middlegem
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jacob
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-07-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rubocop
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.18'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.18'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rubocop-rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.4'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.4'
55
+ - !ruby/object:Gem::Dependency
56
+ name: simplecov
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '='
60
+ - !ruby/object:Gem::Version
61
+ version: '0.17'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '='
67
+ - !ruby/object:Gem::Version
68
+ version: '0.17'
69
+ description: |
70
+ Middlegem is a ruby gem that provides simple middleware chains with goals of simplicity and
71
+ reliability.
72
+ email:
73
+ - jacoblockard99@gmail.com
74
+ executables: []
75
+ extensions: []
76
+ extra_rdoc_files: []
77
+ files:
78
+ - ".gitignore"
79
+ - ".rspec"
80
+ - ".rubocop.yml"
81
+ - ".travis.yml"
82
+ - ".yardopts"
83
+ - CHANGELOG.md
84
+ - Gemfile
85
+ - LICENSE.txt
86
+ - README.md
87
+ - Rakefile
88
+ - bin/console
89
+ - bin/setup
90
+ - lib/middlegem.rb
91
+ - lib/middlegem/array_definition.rb
92
+ - lib/middlegem/definition.rb
93
+ - lib/middlegem/invalid_definition_error.rb
94
+ - lib/middlegem/invalid_middleware_error.rb
95
+ - lib/middlegem/invalid_middleware_output_error.rb
96
+ - lib/middlegem/middleware.rb
97
+ - lib/middlegem/stack.rb
98
+ - lib/middlegem/unpermitted_middleware_error.rb
99
+ - lib/middlegem/version.rb
100
+ - middlegem.gemspec
101
+ homepage: https://github.com/jacoblockard99/middlegem
102
+ licenses:
103
+ - MIT
104
+ metadata:
105
+ homepage_uri: https://github.com/jacoblockard99/middlegem
106
+ source_code_uri: https://github.com/jacoblockard99/middlegem
107
+ changelog_uri: https://github.com/jacoblockard99/middlegem/CHANGELOG.md
108
+ post_install_message:
109
+ rdoc_options: []
110
+ require_paths:
111
+ - lib
112
+ required_ruby_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: 2.5.0
117
+ required_rubygems_version: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - ">="
120
+ - !ruby/object:Gem::Version
121
+ version: '0'
122
+ requirements: []
123
+ rubygems_version: 3.2.3
124
+ signing_key:
125
+ specification_version: 4
126
+ summary: Simple one-way middleware.
127
+ test_files: []