context-pattern 1.0.0 → 1.1.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +26 -0
- data/Gemfile +0 -10
- data/README.md +193 -1
- data/context-pattern.gemspec +1 -1
- data/lib/context-pattern.rb +6 -3
- data/lib/context/base_context_helper.rb +15 -0
- data/lib/context/controller.rb +16 -2
- data/lib/context/version.rb +1 -1
- metadata +6 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f9fcef9a074a4fcc09344918329dc47c42a766a0
|
4
|
+
data.tar.gz: 5f067d37cd45e1f921e24f1e3852e139f85f8011
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5e0818e724627fccb128a91984d6281314101af23378eb138d40467d6be401fabdf450bc90b86c33c271af0d2c703e7d160968c355ab9526a9807ee4e6416c95
|
7
|
+
data.tar.gz: cc29966bdc2088c8d7dd4ec7e22ccc72002b020174c40713f13b26573f8da7837a53e2ebef80fbec3baabdb8ba4db4c4d6f3edf80dfe8d3b8d6960f8e1821595
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
## [1.1.1]
|
4
|
+
* Fix naming bug in BaseContextHelper
|
5
|
+
|
6
|
+
## [1.1.0]
|
7
|
+
* Add method_missing logic to Context::Controller, so that controllers can
|
8
|
+
easily access public methods in the context chain
|
9
|
+
* Add BaseContextHelper module, which is used to provide views access to
|
10
|
+
view_helpers defined in the context chain
|
11
|
+
* Fix gem dependencies in this gemfile
|
12
|
+
* Add README file
|
13
|
+
* Add this changelog
|
14
|
+
|
15
|
+
## [1.0.0]
|
16
|
+
* Prevent contexts from overriding public methods already available in the
|
17
|
+
context chain
|
18
|
+
|
19
|
+
## [0.2.0]
|
20
|
+
* Add specs for BaseContext
|
21
|
+
* Add the introspection methods to BaseContext:
|
22
|
+
#context_chain_class, #context_method_mapping, and #whereis
|
23
|
+
* Add BaseContext.decorate interface
|
24
|
+
|
25
|
+
## [0.1.0]
|
26
|
+
* Initial code ported from the WegoWise app
|
data/Gemfile
CHANGED
@@ -1,12 +1,2 @@
|
|
1
1
|
source 'https://rubygems.org'
|
2
2
|
gemspec
|
3
|
-
|
4
|
-
# The AR environment variable lets you test against different
|
5
|
-
# versions of Rails. For example:
|
6
|
-
#
|
7
|
-
# AR=3.2.13 rm Gemfile.lock && bundle install && bundle exec rspec
|
8
|
-
# AR=4.0.0 rm Gemfile.lock && bundle install && bundle exec rspec
|
9
|
-
#
|
10
|
-
if ENV['AR']
|
11
|
-
gem 'activerecord', ENV['AR']
|
12
|
-
end
|
data/README.md
CHANGED
@@ -1 +1,193 @@
|
|
1
|
-
#
|
1
|
+
# Context pattern
|
2
|
+
|
3
|
+
This gem gives you the scaffolding needed to easily use the Context Pattern in
|
4
|
+
your Ruby on Rails application
|
5
|
+
|
6
|
+
## What is the context pattern?
|
7
|
+
|
8
|
+
The context pattern provides a way of thinking about and writing Rails
|
9
|
+
applications that results in better code that is easier to maintain. This is
|
10
|
+
done through the introduction of a new category of object known as a Context
|
11
|
+
Object.
|
12
|
+
|
13
|
+
A Context Object is responsible for interpreting the current state of the
|
14
|
+
request, providing the context for a controller to do its work, and defining an
|
15
|
+
interface that may be referenced by views. Every request has exactly one
|
16
|
+
context object associated with it. This context is built up throughout the life
|
17
|
+
cycle of a request.
|
18
|
+
|
19
|
+
If you have never encountered the context pattern before, you should read
|
20
|
+
[the explanatory blog post](http://barunsingh.com/2018/03/04/context_pattern.html)
|
21
|
+
to get a thorough understanding of the motivations behind and benefits of this
|
22
|
+
code pattern, examples of before and after code, and a thorough explanation of
|
23
|
+
how everything works.
|
24
|
+
|
25
|
+
This README is intended to provide a reference for those who are already
|
26
|
+
somewhat familiar with the context pattern.
|
27
|
+
|
28
|
+
## Setting up the gem
|
29
|
+
|
30
|
+
To use this gem, you need to do two things:
|
31
|
+
|
32
|
+
1. Require it in your Gemfile:
|
33
|
+
```ruby
|
34
|
+
gem 'context-pattern'
|
35
|
+
```
|
36
|
+
|
37
|
+
2. Add the following two lines to your `ApplicationController`:
|
38
|
+
```ruby
|
39
|
+
include Context::Controller
|
40
|
+
helper Context::BaseContextHelper
|
41
|
+
```
|
42
|
+
|
43
|
+
## Simple example
|
44
|
+
|
45
|
+
The example below is a simple one that is used to demonstrate various facets
|
46
|
+
of how this gem and the context pattern work. Suppose we have an online
|
47
|
+
bookstore and are looking at a `BooksController#show` action. We want to
|
48
|
+
retrieve the logged in user from the session and the book being viewed from the
|
49
|
+
params. We use a decorator to provide some functionality around showing the
|
50
|
+
user's name (this is contrived, but demonstrative).
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
class ApplicationController < ActionController::Base
|
54
|
+
include Context::Controller
|
55
|
+
helper Context::BaseContextHelper
|
56
|
+
|
57
|
+
before_action :set_application_context
|
58
|
+
|
59
|
+
def set_application_context
|
60
|
+
extend_context :Application, params: params, session: session
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class BooksController < ApplicationController
|
65
|
+
def show
|
66
|
+
extend_context :BookShow
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class ApplicationContext < Context::BaseContext
|
71
|
+
view_helpers :current_user
|
72
|
+
|
73
|
+
attr_accessor :session, :params
|
74
|
+
|
75
|
+
def current_user
|
76
|
+
User.find_by(id: session[:user_id])
|
77
|
+
end
|
78
|
+
memoize :current_user
|
79
|
+
end
|
80
|
+
|
81
|
+
class BookShowContext < Context::BaseContext
|
82
|
+
view_helpers :book
|
83
|
+
|
84
|
+
decorate :current_user, decorator: UserPresenter, memoize: true
|
85
|
+
|
86
|
+
def book
|
87
|
+
Book.find(params[:id])
|
88
|
+
end
|
89
|
+
memoize :book
|
90
|
+
end
|
91
|
+
|
92
|
+
class UserPresenter < SimpleDelegator
|
93
|
+
def abbreviated_name
|
94
|
+
"#{first_name} #{last_name[0]}"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
```
|
98
|
+
|
99
|
+
View file:
|
100
|
+
```erb
|
101
|
+
Hi, <%= current_user.abbreviated_name %>.
|
102
|
+
Here is information about <%= book.title %>
|
103
|
+
```
|
104
|
+
|
105
|
+
## Basic components of using the gem
|
106
|
+
|
107
|
+
* All context classes must inherit from `Context::BaseContext`
|
108
|
+
* To add a context to the context stack, use `extend_context`. Usage example:
|
109
|
+
```ruby
|
110
|
+
extend_context :Foo, arg1: 1, arg2: 2
|
111
|
+
# The above is equivalent to adding the following object to the context stack:
|
112
|
+
# FooContext.new(arg1: 1, arg2: 2)
|
113
|
+
```
|
114
|
+
* If you want to be able to provide arguments when initializing a context as
|
115
|
+
in the example above, your context class needs to use `attr_accessor` to
|
116
|
+
declare those attribute names.
|
117
|
+
* A context object has access to all public methods already defined in the
|
118
|
+
context stack. It does not have access to any non-public methods used by
|
119
|
+
other objects in the context stack.
|
120
|
+
* The order in which you add to the context stack is important. While a context
|
121
|
+
object can reference public methods from earlier in the context stack, it can
|
122
|
+
not make reference to public methods from objects added later to the context
|
123
|
+
stack.
|
124
|
+
* Controllers have access to all public methods defined anywhere in the context
|
125
|
+
stack.
|
126
|
+
* Views have access to all public methods in the context stack that are declared
|
127
|
+
to be `view_helpers`.
|
128
|
+
* Methods do not necessarily need to be defined in the same context in which
|
129
|
+
they are declarated to be `view_helpers`. But a method must be available to
|
130
|
+
the context in which it is declared to be a view helper. This means the method
|
131
|
+
must either be defined in that context or in a context that is already part
|
132
|
+
of the context stack at the time.
|
133
|
+
* A context can not overwrite a public method that is already defined in the
|
134
|
+
context stack. Trying to do so will cause a `Context::MethodOverrideError`
|
135
|
+
exception to be raised.
|
136
|
+
* The `decorate` declaration provides a way to get around the above restriction
|
137
|
+
in situations where we reasonably wish to decorate or present an object
|
138
|
+
already available in the context stack. This declaration may be used as
|
139
|
+
follows:
|
140
|
+
```ruby
|
141
|
+
class BlahContext < Context::BaseContext
|
142
|
+
# Suppose `foo` is a public method already available in the context stack
|
143
|
+
decorate :foo, decorator: FooDecorator, args: [:bar, :baz], memoize: true
|
144
|
+
|
145
|
+
# The above is functionaly equivalent to the code below:
|
146
|
+
# def foo
|
147
|
+
# FooDecorator.new(@parent_context.foo, bar: bar, baz: baz)
|
148
|
+
# end
|
149
|
+
# memoize :foo
|
150
|
+
|
151
|
+
def bar; end
|
152
|
+
def baz; end
|
153
|
+
end
|
154
|
+
```
|
155
|
+
* You can reference application routes in your context objects.
|
156
|
+
`Context::BaseContext` includes `Rails.application.routes.url_helpers`. You
|
157
|
+
can also use `link_to` within your contexts.
|
158
|
+
|
159
|
+
|
160
|
+
## Best practices for usage
|
161
|
+
|
162
|
+
The following suggestions are not requirements for using this gem, but bits of
|
163
|
+
advice that have been pulled together from using the context pattern across
|
164
|
+
the WegoWise codebase over a period of five years.
|
165
|
+
|
166
|
+
* You should have something like an `ApplicationContext` that takes params,
|
167
|
+
session, etc. as arguments on initialization. The example in this README
|
168
|
+
shows a simple version of this. If you do this, all later contexts will have
|
169
|
+
access to the params, which will greatly simplify things.
|
170
|
+
* Aside from `ApplicationContext`, you should almost never need to provide any
|
171
|
+
arguments to a context when initializing it via `extend_context`. This means
|
172
|
+
those contexts shouldn't make use of `attr_accessor`. There may be some
|
173
|
+
exceptions, but generally speaking a context should be able to figure out
|
174
|
+
everything it needs from `params` and methods already available via the
|
175
|
+
context stack.
|
176
|
+
* If you find yourself wanting to override methods from earlier contexts in
|
177
|
+
ways that do not follow the decorator pattern, this is a sign you are not
|
178
|
+
thinking about your code properly. Sometimes this may simply be a matter of
|
179
|
+
having different method names for different concepts, Other times it may mean
|
180
|
+
that your contexts are conceptually ambiguous.
|
181
|
+
* It is a best practice to add a comment at the top of each context file stating
|
182
|
+
in plain language what the context is for the usage of that object. This
|
183
|
+
should not need to be more than a couple short sentences. If you are having
|
184
|
+
difficulty doing this, it may be a sign that you are trying to do too much
|
185
|
+
within a single context object.
|
186
|
+
* `memoize` is made available via the `memoizer` gem, which is a dependency of
|
187
|
+
this gem. It is a best practice to memoize all view helpers that do any sort
|
188
|
+
of work, and to memoize objects that use the `decorate` declaration.
|
189
|
+
|
190
|
+
|
191
|
+
## How to test context objects
|
192
|
+
|
193
|
+
To be filled in soon.
|
data/context-pattern.gemspec
CHANGED
@@ -16,7 +16,7 @@ Gem::Specification.new do |s|
|
|
16
16
|
s.extra_rdoc_files = ["README.md", "LICENSE.txt"]
|
17
17
|
s.license = 'MIT'
|
18
18
|
|
19
|
-
s.add_dependency('rails', '>=
|
19
|
+
s.add_dependency('rails', '>= 4.0')
|
20
20
|
s.add_dependency('memoizer')
|
21
21
|
|
22
22
|
s.add_development_dependency('rspec', '~> 3.0')
|
data/lib/context-pattern.rb
CHANGED
@@ -1,4 +1,7 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
require_relative 'context/base_context'
|
2
|
+
require_relative 'context/controller'
|
3
3
|
|
4
|
-
|
4
|
+
if defined?(Rails)
|
5
|
+
require_relative 'context/railtie'
|
6
|
+
require_relative 'context/base_context_helper'
|
7
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Context
|
2
|
+
module BaseContextHelper
|
3
|
+
def method_missing(method_name, *args, &block)
|
4
|
+
if @__context && @__context.has_view_helper?(method_name)
|
5
|
+
@__context.public_send(method_name, *args, &block)
|
6
|
+
else
|
7
|
+
super
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def respond_to_missing?(method_name, _include_private = false)
|
12
|
+
@__context.has_view_helper?(method_name)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/context/controller.rb
CHANGED
@@ -6,11 +6,25 @@ module Context
|
|
6
6
|
|
7
7
|
def extend_context(context, **args)
|
8
8
|
context_class = "#{context}Context".constantize
|
9
|
-
@
|
9
|
+
@__context = context_class.wrap(@__context, **args)
|
10
10
|
end
|
11
11
|
|
12
12
|
def __set_base_context
|
13
|
-
@
|
13
|
+
@__context = Context::BaseContext.new
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def method_missing(method_name, *args, &block)
|
19
|
+
if @__context.respond_to?(method_name)
|
20
|
+
@__context.public_send(method_name, *args, &block)
|
21
|
+
else
|
22
|
+
super
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def respond_to_missing?(method_name, include_private = false)
|
27
|
+
@__context.respond_to?(method_name, include_private)
|
14
28
|
end
|
15
29
|
end
|
16
30
|
end
|
data/lib/context/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: context-pattern
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Barun Singh
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-03-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -16,20 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
20
|
-
- - "<"
|
21
|
-
- !ruby/object:Gem::Version
|
22
|
-
version: '5.2'
|
19
|
+
version: '4.0'
|
23
20
|
type: :runtime
|
24
21
|
prerelease: false
|
25
22
|
version_requirements: !ruby/object:Gem::Requirement
|
26
23
|
requirements:
|
27
24
|
- - ">="
|
28
25
|
- !ruby/object:Gem::Version
|
29
|
-
version: '
|
30
|
-
- - "<"
|
31
|
-
- !ruby/object:Gem::Version
|
32
|
-
version: '5.2'
|
26
|
+
version: '4.0'
|
33
27
|
- !ruby/object:Gem::Dependency
|
34
28
|
name: memoizer
|
35
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -81,6 +75,7 @@ extra_rdoc_files:
|
|
81
75
|
- LICENSE.txt
|
82
76
|
files:
|
83
77
|
- ".gitignore"
|
78
|
+
- CHANGELOG.md
|
84
79
|
- Gemfile
|
85
80
|
- LICENSE.txt
|
86
81
|
- README.md
|
@@ -88,6 +83,7 @@ files:
|
|
88
83
|
- context-pattern.gemspec
|
89
84
|
- lib/context-pattern.rb
|
90
85
|
- lib/context/base_context.rb
|
86
|
+
- lib/context/base_context_helper.rb
|
91
87
|
- lib/context/controller.rb
|
92
88
|
- lib/context/railtie.rb
|
93
89
|
- lib/context/version.rb
|