stitches 3.7.3 → 4.0.0.RC1
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/.circleci/config.yml +35 -30
- data/{CODEOWNERS → .github/CODEOWNERS} +1 -1
- data/.github/PULL_REQUEST_TEMPLATE.md +7 -0
- data/.gitignore +1 -0
- data/.ruby-version +1 -1
- data/.travis.yml +5 -2
- data/Gemfile.rails-5.1 +1 -2
- data/Gemfile.rails-5.2 +1 -2
- data/Gemfile.rails-6.0 +7 -0
- data/README.md +38 -28
- data/lib/stitches.rb +0 -2
- data/lib/stitches/allowlist_middleware.rb +0 -1
- data/lib/stitches/api_client_access_wrapper.rb +42 -0
- data/lib/stitches/api_generator.rb +31 -29
- data/lib/stitches/api_key.rb +20 -20
- data/lib/stitches/configuration.rb +35 -1
- data/lib/stitches/generator_files/config/initializers/stitches.rb +10 -0
- data/lib/stitches/generator_files/spec/features/api_spec.rb.erb +10 -2
- data/lib/stitches/railtie.rb +2 -0
- data/lib/stitches/valid_mime_type.rb +6 -5
- data/lib/stitches/version.rb +3 -1
- data/owners.json +1 -1
- data/spec/api_client_access_wrapper_spec.rb +52 -0
- data/spec/api_key_spec.rb +13 -12
- data/spec/configuration_spec.rb +45 -0
- data/spec/integration/add_to_rails_app_spec.rb +19 -18
- data/spec/valid_mime_type_spec.rb +4 -3
- data/stitches.gemspec +2 -3
- metadata +18 -29
- data/PULL_REQUEST_TEMPLATE.md +0 -21
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d181adb5b8ed9642468e2a8c75ab5ed2cbb01756458885410c8538c0b0a51890
|
|
4
|
+
data.tar.gz: 95a4105d911e4e4ebbf927d2bf87efb8bc6dfb1d2537e4f0ebc42caf8569c44d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 442b3c89184c500c7036bb06a355c0df478ea16089777d920aefc62c94dedb37ad0be40a4a46dc558509148b0f08feb335cece9e8e79ac732482977876931d99
|
|
7
|
+
data.tar.gz: aeaf701f19b4e8962227cccd7421f3452bf7e2dde77bfa2e7ecce735883adade7903a3392009d351e48258d2b6f231f36d36190a8e65c184e8ad5b9681d94f7f
|
data/.circleci/config.yml
CHANGED
|
@@ -5,9 +5,10 @@ version: 2
|
|
|
5
5
|
jobs:
|
|
6
6
|
release:
|
|
7
7
|
docker:
|
|
8
|
-
- image: circleci/ruby:2.
|
|
8
|
+
- image: circleci/ruby:2.7.1
|
|
9
9
|
steps:
|
|
10
10
|
- checkout
|
|
11
|
+
- run: bundle config stitchfix01.jfrog.io $ARTIFACTORY_USER:$ARTIFACTORY_TOKEN
|
|
11
12
|
- run: bundle install --full-index
|
|
12
13
|
- run:
|
|
13
14
|
name: Artifactory login
|
|
@@ -16,14 +17,15 @@ jobs:
|
|
|
16
17
|
- run:
|
|
17
18
|
name: Build/release gem to artifactory
|
|
18
19
|
command: bundle exec rake push_artifactory
|
|
19
|
-
ruby-2.
|
|
20
|
+
ruby-2.7.1-rails-6.0:
|
|
20
21
|
docker:
|
|
21
|
-
- image: circleci/ruby:2.
|
|
22
|
+
- image: circleci/ruby:2.7.1
|
|
22
23
|
environment:
|
|
23
|
-
BUNDLE_GEMFILE: Gemfile.rails-
|
|
24
|
+
BUNDLE_GEMFILE: Gemfile.rails-6.0
|
|
24
25
|
working_directory: "~/stitches"
|
|
25
26
|
steps:
|
|
26
27
|
- checkout
|
|
28
|
+
- run: bundle config stitchfix01.jfrog.io $ARTIFACTORY_USER:$ARTIFACTORY_TOKEN
|
|
27
29
|
- run: bundle install --full-index
|
|
28
30
|
- run: bundle exec rspec --format RspecJunitFormatter --out /tmp/test-results/rspec.xml
|
|
29
31
|
--format=doc
|
|
@@ -33,18 +35,19 @@ jobs:
|
|
|
33
35
|
fi
|
|
34
36
|
- run:
|
|
35
37
|
name: Notify Pager Duty
|
|
36
|
-
command: bundle exec y-notify "#
|
|
38
|
+
command: bundle exec y-notify "#devex-alerts"
|
|
37
39
|
when: on_fail
|
|
38
40
|
- store_test_results:
|
|
39
41
|
path: "/tmp/test-results"
|
|
40
|
-
ruby-2.
|
|
42
|
+
ruby-2.6.6-rails-6.0:
|
|
41
43
|
docker:
|
|
42
|
-
- image: circleci/ruby:2.
|
|
44
|
+
- image: circleci/ruby:2.6.6
|
|
43
45
|
environment:
|
|
44
|
-
BUNDLE_GEMFILE: Gemfile.rails-
|
|
46
|
+
BUNDLE_GEMFILE: Gemfile.rails-6.0
|
|
45
47
|
working_directory: "~/stitches"
|
|
46
48
|
steps:
|
|
47
49
|
- checkout
|
|
50
|
+
- run: bundle config stitchfix01.jfrog.io $ARTIFACTORY_USER:$ARTIFACTORY_TOKEN
|
|
48
51
|
- run: bundle install --full-index
|
|
49
52
|
- run: bundle exec rspec --format RspecJunitFormatter --out /tmp/test-results/rspec.xml
|
|
50
53
|
--format=doc
|
|
@@ -54,18 +57,19 @@ jobs:
|
|
|
54
57
|
fi
|
|
55
58
|
- run:
|
|
56
59
|
name: Notify Pager Duty
|
|
57
|
-
command: bundle exec y-notify "#
|
|
60
|
+
command: bundle exec y-notify "#devex-alerts"
|
|
58
61
|
when: on_fail
|
|
59
62
|
- store_test_results:
|
|
60
63
|
path: "/tmp/test-results"
|
|
61
|
-
ruby-2.
|
|
64
|
+
ruby-2.7.1-rails-5.2:
|
|
62
65
|
docker:
|
|
63
|
-
- image: circleci/ruby:2.
|
|
66
|
+
- image: circleci/ruby:2.7.1
|
|
64
67
|
environment:
|
|
65
|
-
BUNDLE_GEMFILE: Gemfile.rails-5.
|
|
68
|
+
BUNDLE_GEMFILE: Gemfile.rails-5.2
|
|
66
69
|
working_directory: "~/stitches"
|
|
67
70
|
steps:
|
|
68
71
|
- checkout
|
|
72
|
+
- run: bundle config stitchfix01.jfrog.io $ARTIFACTORY_USER:$ARTIFACTORY_TOKEN
|
|
69
73
|
- run: bundle install --full-index
|
|
70
74
|
- run: bundle exec rspec --format RspecJunitFormatter --out /tmp/test-results/rspec.xml
|
|
71
75
|
--format=doc
|
|
@@ -75,18 +79,19 @@ jobs:
|
|
|
75
79
|
fi
|
|
76
80
|
- run:
|
|
77
81
|
name: Notify Pager Duty
|
|
78
|
-
command: bundle exec y-notify "#
|
|
82
|
+
command: bundle exec y-notify "#devex-alerts"
|
|
79
83
|
when: on_fail
|
|
80
84
|
- store_test_results:
|
|
81
85
|
path: "/tmp/test-results"
|
|
82
|
-
ruby-2.
|
|
86
|
+
ruby-2.6.6-rails-5.2:
|
|
83
87
|
docker:
|
|
84
|
-
- image: circleci/ruby:2.
|
|
88
|
+
- image: circleci/ruby:2.6.6
|
|
85
89
|
environment:
|
|
86
|
-
BUNDLE_GEMFILE: Gemfile.rails-5.
|
|
90
|
+
BUNDLE_GEMFILE: Gemfile.rails-5.2
|
|
87
91
|
working_directory: "~/stitches"
|
|
88
92
|
steps:
|
|
89
93
|
- checkout
|
|
94
|
+
- run: bundle config stitchfix01.jfrog.io $ARTIFACTORY_USER:$ARTIFACTORY_TOKEN
|
|
90
95
|
- run: bundle install --full-index
|
|
91
96
|
- run: bundle exec rspec --format RspecJunitFormatter --out /tmp/test-results/rspec.xml
|
|
92
97
|
--format=doc
|
|
@@ -96,7 +101,7 @@ jobs:
|
|
|
96
101
|
fi
|
|
97
102
|
- run:
|
|
98
103
|
name: Notify Pager Duty
|
|
99
|
-
command: bundle exec y-notify "#
|
|
104
|
+
command: bundle exec y-notify "#devex-alerts"
|
|
100
105
|
when: on_fail
|
|
101
106
|
- store_test_results:
|
|
102
107
|
path: "/tmp/test-results"
|
|
@@ -107,31 +112,31 @@ workflows:
|
|
|
107
112
|
- release:
|
|
108
113
|
context: org-global
|
|
109
114
|
requires:
|
|
110
|
-
- ruby-2.
|
|
111
|
-
- ruby-2.
|
|
112
|
-
- ruby-2.
|
|
113
|
-
- ruby-2.
|
|
115
|
+
- ruby-2.7.1-rails-6.0
|
|
116
|
+
- ruby-2.6.6-rails-6.0
|
|
117
|
+
- ruby-2.7.1-rails-5.2
|
|
118
|
+
- ruby-2.6.6-rails-5.2
|
|
114
119
|
filters:
|
|
115
120
|
tags:
|
|
116
|
-
only: /^[0-9]+\.[0-9]+\.[0-9](
|
|
121
|
+
only: /^[0-9]+\.[0-9]+\.[0-9]+(\.?(RC|rc)[-\.]?\d*)?$/
|
|
117
122
|
branches:
|
|
118
123
|
ignore: /.*/
|
|
119
|
-
- ruby-2.
|
|
124
|
+
- ruby-2.7.1-rails-6.0:
|
|
120
125
|
context: org-global
|
|
121
126
|
filters:
|
|
122
127
|
tags:
|
|
123
128
|
only: &1 /.*/
|
|
124
|
-
- ruby-2.
|
|
129
|
+
- ruby-2.6.6-rails-6.0:
|
|
125
130
|
context: org-global
|
|
126
131
|
filters:
|
|
127
132
|
tags:
|
|
128
133
|
only: *1
|
|
129
|
-
- ruby-2.
|
|
134
|
+
- ruby-2.7.1-rails-5.2:
|
|
130
135
|
context: org-global
|
|
131
136
|
filters:
|
|
132
137
|
tags:
|
|
133
138
|
only: *1
|
|
134
|
-
- ruby-2.
|
|
139
|
+
- ruby-2.6.6-rails-5.2:
|
|
135
140
|
context: org-global
|
|
136
141
|
filters:
|
|
137
142
|
tags:
|
|
@@ -145,11 +150,11 @@ workflows:
|
|
|
145
150
|
only:
|
|
146
151
|
- master
|
|
147
152
|
jobs:
|
|
148
|
-
- ruby-2.
|
|
153
|
+
- ruby-2.7.1-rails-6.0:
|
|
149
154
|
context: org-global
|
|
150
|
-
- ruby-2.
|
|
155
|
+
- ruby-2.6.6-rails-6.0:
|
|
151
156
|
context: org-global
|
|
152
|
-
- ruby-2.
|
|
157
|
+
- ruby-2.7.1-rails-5.2:
|
|
153
158
|
context: org-global
|
|
154
|
-
- ruby-2.
|
|
159
|
+
- ruby-2.6.6-rails-5.2:
|
|
155
160
|
context: org-global
|
data/.gitignore
CHANGED
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.
|
|
1
|
+
2.7.1
|
data/.travis.yml
CHANGED
data/Gemfile.rails-5.1
CHANGED
data/Gemfile.rails-5.2
CHANGED
data/Gemfile.rails-6.0
ADDED
data/README.md
CHANGED
|
@@ -4,13 +4,13 @@ Create Microservices in Rails by pretty much just writing regular Rails code.
|
|
|
4
4
|
|
|
5
5
|
This gem provides:
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
- transparent API key authentication.
|
|
8
|
+
- router-level API version based on headers.
|
|
9
|
+
- a way to document your microservice endpoints via acceptance tests.
|
|
10
|
+
- structured errors, buildable from invalid Active Records, Exceptions, or by hand.
|
|
11
11
|
|
|
12
12
|
This, plus much of what you get from Rails already, means you can create a microservice Rails application by just writing the
|
|
13
|
-
same Rails code you write today.
|
|
13
|
+
same Rails code you write today. Instead of rendering web views, you render JSON (which is built into Rails).
|
|
14
14
|
|
|
15
15
|
## To install
|
|
16
16
|
|
|
@@ -29,22 +29,32 @@ bundle install
|
|
|
29
29
|
Then, set it up:
|
|
30
30
|
|
|
31
31
|
```
|
|
32
|
-
> bin/rails generate rspec:install
|
|
33
|
-
> bin/rails generate apitome:install
|
|
34
32
|
> bin/rails generate stitches:api
|
|
35
33
|
> bundle exec rake db:migrate
|
|
36
34
|
```
|
|
37
35
|
|
|
38
36
|
### Upgrading from an older version
|
|
39
37
|
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
- When upgrading to version 4.0.0 you may now take advantage of an in-memory cache
|
|
39
|
+
|
|
40
|
+
You can enabled it like so
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
Stitches.configure do |config|
|
|
44
|
+
config.max_cache_ttl = 5 # seconds
|
|
45
|
+
config.max_cache_size = 100 # how many keys to cache
|
|
46
|
+
end
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
- If you have a version lower than 3.3.0, you need to run two generators, one of which creates a new database migration on your
|
|
50
|
+
`api_clients` table:
|
|
42
51
|
|
|
43
52
|
```
|
|
44
53
|
> bin/rails generate stitches:add_enabled_to_api_clients
|
|
45
54
|
> bin/rails generate stitches:add_deprecation
|
|
46
55
|
```
|
|
47
|
-
|
|
56
|
+
|
|
57
|
+
- If you have a version lower than 3.6.0, you need to run one generator:
|
|
48
58
|
|
|
49
59
|
```
|
|
50
60
|
> bin/rails generate stitches:add_deprecation
|
|
@@ -61,8 +71,8 @@ class Api::V1::WidgetsController < ApiController
|
|
|
61
71
|
if widget.valid?
|
|
62
72
|
head 201
|
|
63
73
|
else
|
|
64
|
-
render json: {
|
|
65
|
-
errors: Stitches::Errors.from_active_record_object(widget)
|
|
74
|
+
render json: {
|
|
75
|
+
errors: Stitches::Errors.from_active_record_object(widget)
|
|
66
76
|
}, status: 422
|
|
67
77
|
end
|
|
68
78
|
end
|
|
@@ -75,44 +85,44 @@ private
|
|
|
75
85
|
end
|
|
76
86
|
```
|
|
77
87
|
|
|
78
|
-
If you think there's nothing special about this—you are correct.
|
|
88
|
+
If you think there's nothing special about this—you are correct. This is the vanillaest of vanilla Rails controllers, with a few
|
|
79
89
|
notable exceptions:
|
|
80
90
|
|
|
81
|
-
|
|
82
|
-
you, so there's zero need to use `respond_to` and friends.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
about it in your code.
|
|
86
|
-
|
|
87
|
-
routed here.
|
|
91
|
+
- We aren't checking content type. A stitches-based microservice always uses JSON and refuses to route requests for non-JSON to
|
|
92
|
+
you, so there's zero need to use `respond_to` and friends.
|
|
93
|
+
- The error-building is structured and reliable.
|
|
94
|
+
- This is an authenticated request. No request without proper authentication will be routed here, so you don't have to worry
|
|
95
|
+
about it in your code.
|
|
96
|
+
- This is a versioned request. While the URL will _not_ contain `v1` in it, the `Accept` header will require a version and get
|
|
97
|
+
routed here. If you make a V2, it's just a new controller and this concern is handled at the routing layer.
|
|
88
98
|
|
|
89
|
-
All this means that the Rails skills of you and your team can be directly applied to building microservices.
|
|
99
|
+
All this means that the Rails skills of you and your team can be directly applied to building microservices. You don't have to make a bunch of boring decisions about auth, versioning, or content-types. It also means you can start deploying and creating microservices with little friction. No need to deal with a complex DSL or new programming language to get yourselves going with Microservices.
|
|
90
100
|
|
|
91
101
|
## More Info
|
|
92
102
|
|
|
93
103
|
See [the wiki](https://github.com/stitchfix/stitches/wiki/Setup) for how to setup stitches.
|
|
94
104
|
|
|
95
|
-
|
|
105
|
+
- [Stitches Features](https://github.com/stitchfix/stitches/wiki/Features-of-Stitches) include:
|
|
96
106
|
- Authorization via API key
|
|
97
107
|
- Versioned requests via HTTP content types
|
|
98
108
|
- Structured Errors
|
|
99
109
|
- ISO 8601-formatted dates
|
|
100
110
|
- Deprecation using the `Sunset` header
|
|
101
|
-
|
|
102
|
-
|
|
111
|
+
- An optional ApiKey cache to allow mostly DB free APIs
|
|
112
|
+
- The [Generator](https://github.com/stitchfix/stitches/wiki/Generator) sets up some code in your app, so you can start writing
|
|
113
|
+
APIs using vanilla Rails idioms:
|
|
103
114
|
- a "ping" controller that can validate your app is working
|
|
104
115
|
- version routing based on content-type (requests for V2 use the same URL, but are serviced by a different controller)
|
|
105
116
|
- An ApiClient Active Record
|
|
106
117
|
- Acceptance tests that can produce API documentation as they test your app.
|
|
107
|
-
|
|
108
|
-
|
|
118
|
+
- Stitches provides [testing support](https://github.com/stitchfix/stitches/wiki/Testing)
|
|
109
119
|
|
|
110
120
|
## Developing
|
|
111
121
|
|
|
112
|
-
Although `Stitches.configuration` is global, do not depend directly on that in your logic.
|
|
122
|
+
Although `Stitches.configuration` is global, do not depend directly on that in your logic. Instead, allow all classes to receive a configuration object in their constructor. This makes the classes easier to deal with and change, without incurring much of a real cost to development. Global symbols suck, but are convenient. This is how you make the most of it.
|
|
113
123
|
|
|
114
124
|
Also, the integration test does a lot of "testing the implementation", but since Rails generators are notorious for silently
|
|
115
|
-
failing with a successful result, we have to make sure that the various `inject_into_file` calls are actually working.
|
|
125
|
+
failing with a successful result, we have to make sure that the various `inject_into_file` calls are actually working. Do not do
|
|
116
126
|
any fancy refactors here, just keep it up to date.
|
|
117
127
|
|
|
118
128
|
---
|
data/lib/stitches.rb
CHANGED
|
@@ -2,7 +2,6 @@ module Stitches
|
|
|
2
2
|
# A middleware that will skip its behavior if the path matches an allowed URL
|
|
3
3
|
class AllowlistMiddleware
|
|
4
4
|
def initialize(app, options={})
|
|
5
|
-
|
|
6
5
|
@app = app
|
|
7
6
|
@configuration = options[:configuration] || Stitches.configuration
|
|
8
7
|
@except = options[:except] || @configuration.allowlist_regexp
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
require 'lru_redux'
|
|
2
|
+
|
|
3
|
+
module Stitches::ApiClientAccessWrapper
|
|
4
|
+
|
|
5
|
+
def self.fetch_for_key(key)
|
|
6
|
+
if cache_enabled
|
|
7
|
+
fetch_for_key_from_cache(key)
|
|
8
|
+
else
|
|
9
|
+
fetch_for_key_from_db(key)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.fetch_for_key_from_cache(key)
|
|
14
|
+
api_key_cache.getset(key) do
|
|
15
|
+
fetch_for_key_from_db(key)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.fetch_for_key_from_db(key)
|
|
20
|
+
if ::ApiClient.column_names.include?("enabled")
|
|
21
|
+
::ApiClient.find_by(key: key, enabled: true)
|
|
22
|
+
else
|
|
23
|
+
ActiveSupport::Deprecation.warn('api_keys is missing "enabled" column. Run "rails g stitches:add_enabled_to_api_clients"')
|
|
24
|
+
::ApiClient.find_by(key: key)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.clear_api_cache
|
|
29
|
+
api_key_cache.clear if cache_enabled
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.api_key_cache
|
|
33
|
+
@api_key_cache ||= LruRedux::TTL::ThreadSafeCache.new(
|
|
34
|
+
Stitches.configuration.max_cache_size,
|
|
35
|
+
Stitches.configuration.max_cache_ttl,
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.cache_enabled
|
|
40
|
+
Stitches.configuration.max_cache_ttl.positive?
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -4,7 +4,7 @@ module Stitches
|
|
|
4
4
|
class ApiGenerator < Rails::Generators::Base
|
|
5
5
|
include Rails::Generators::Migration
|
|
6
6
|
|
|
7
|
-
source_root(File.expand_path(File.join(File.dirname(__FILE__),"generator_files")))
|
|
7
|
+
source_root(File.expand_path(File.join(File.dirname(__FILE__), "generator_files")))
|
|
8
8
|
|
|
9
9
|
def self.next_migration_number(path)
|
|
10
10
|
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
|
@@ -12,14 +12,22 @@ module Stitches
|
|
|
12
12
|
|
|
13
13
|
desc "Bootstraps your API service with a basic ping controller and spec to ensure everything is setup properly"
|
|
14
14
|
def bootstrap_api
|
|
15
|
-
|
|
15
|
+
gem "apitome"
|
|
16
|
+
gem_group :development, :test do
|
|
17
|
+
gem "rspec"
|
|
18
|
+
gem "rspec-rails"
|
|
19
|
+
gem "rspec_api_documentation"
|
|
20
|
+
end
|
|
16
21
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
gem "rspec_api_documentation", group: [ :development, :test ]
|
|
20
|
-
gem "capybara", group: [ :development, :test ]
|
|
21
|
-
GEM
|
|
22
|
+
Bundler.with_clean_env do
|
|
23
|
+
run "bundle install"
|
|
22
24
|
end
|
|
25
|
+
generate "apitome:install"
|
|
26
|
+
generate "rspec:install"
|
|
27
|
+
|
|
28
|
+
gsub_file 'config/initializers/apitome.rb', /config.mount_at = .*$/, "config.mount_at = nil"
|
|
29
|
+
gsub_file 'config/initializers/apitome.rb', /config.title = .*$/, "config.title = 'Service Documentation'"
|
|
30
|
+
|
|
23
31
|
inject_into_file "config/routes.rb", before: /^end/ do<<-ROUTES
|
|
24
32
|
namespace :api do
|
|
25
33
|
scope module: :v1, constraints: Stitches::ApiVersionConstraint.new(1) do
|
|
@@ -32,15 +40,14 @@ namespace :api do
|
|
|
32
40
|
# as well as for your client to be able to validate this as well.
|
|
33
41
|
end
|
|
34
42
|
end
|
|
35
|
-
|
|
43
|
+
|
|
44
|
+
api_docs = Rack::Auth::Basic.new(Apitome::Engine) do |_, password|
|
|
36
45
|
password == ENV['HTTP_AUTH_PASSWORD']
|
|
37
46
|
end
|
|
38
47
|
mount api_docs, at: "docs"
|
|
39
48
|
ROUTES
|
|
40
49
|
end
|
|
41
50
|
|
|
42
|
-
run 'bundle install'
|
|
43
|
-
|
|
44
51
|
copy_file "app/controllers/api.rb"
|
|
45
52
|
copy_file "app/controllers/api/api_controller.rb"
|
|
46
53
|
copy_file "app/controllers/api/v1.rb"
|
|
@@ -53,8 +60,6 @@ mount api_docs, at: "docs"
|
|
|
53
60
|
template "spec/features/api_spec.rb.erb", "spec/features/api_spec.rb"
|
|
54
61
|
copy_file "spec/acceptance/ping_v1_spec.rb", "spec/acceptance/ping_v1_spec.rb"
|
|
55
62
|
|
|
56
|
-
run 'bundle install'
|
|
57
|
-
|
|
58
63
|
migration_template "db/migrate/enable_uuid_ossp_extension.rb", "db/migrate/enable_uuid_ossp_extension.rb"
|
|
59
64
|
sleep 1 # allow clock to tick so we get different numbers
|
|
60
65
|
migration_template "db/migrate/create_api_clients.rb", "db/migrate/create_api_clients.rb"
|
|
@@ -70,26 +75,23 @@ require 'stitches/spec'
|
|
|
70
75
|
|
|
71
76
|
append_to_file 'spec/rails_helper.rb' do<<-RSPEC_API
|
|
72
77
|
require 'rspec_api_documentation'
|
|
78
|
+
|
|
73
79
|
RspecApiDocumentation.configure do |config|
|
|
74
|
-
config.format = :json
|
|
75
|
-
config.request_headers_to_include = %w(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
)
|
|
81
|
-
config.response_headers_to_include = %w(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
)
|
|
85
|
-
config.api_name = "YOUR SERVICE NAME HERE"
|
|
80
|
+
config.format = :json
|
|
81
|
+
config.request_headers_to_include = %w(
|
|
82
|
+
Accept
|
|
83
|
+
Content-Type
|
|
84
|
+
Authorization
|
|
85
|
+
If-Modified-Since
|
|
86
|
+
)
|
|
87
|
+
config.response_headers_to_include = %w(
|
|
88
|
+
Last-Modified
|
|
89
|
+
ETag
|
|
90
|
+
)
|
|
91
|
+
config.api_name = "YOUR SERVICE NAME HERE"
|
|
86
92
|
end
|
|
87
|
-
|
|
93
|
+
RSPEC_API
|
|
88
94
|
end
|
|
89
|
-
run "rails g apitome:install"
|
|
90
|
-
gsub_file 'config/initializers/apitome.rb', /config.mount_at = .*$/, "config.mount_at = nil"
|
|
91
|
-
gsub_file 'config/initializers/apitome.rb', /config.title = .*$/, "config.title = 'Service Documentation'"
|
|
92
|
-
|
|
93
95
|
end
|
|
94
96
|
end
|
|
95
97
|
end
|
data/lib/stitches/api_key.rb
CHANGED
|
@@ -20,11 +20,6 @@ module Stitches
|
|
|
20
20
|
# ApiClient that it maps to.
|
|
21
21
|
class ApiKey < Stitches::AllowlistMiddleware
|
|
22
22
|
|
|
23
|
-
def initialize(app,options = {})
|
|
24
|
-
super(app,options)
|
|
25
|
-
@realm = Rails.application.class.parent.to_s
|
|
26
|
-
end
|
|
27
|
-
|
|
28
23
|
protected
|
|
29
24
|
|
|
30
25
|
def do_call(env)
|
|
@@ -32,35 +27,40 @@ module Stitches
|
|
|
32
27
|
if authorization
|
|
33
28
|
if authorization =~ /#{@configuration.custom_http_auth_scheme}\s+key=(.*)\s*$/
|
|
34
29
|
key = $1
|
|
35
|
-
|
|
36
|
-
if ApiClient.column_names.include?("enabled")
|
|
37
|
-
client = ApiClient.where(key: key, enabled: true).first
|
|
38
|
-
else
|
|
39
|
-
ActiveSupport::Deprecation.warn('api_keys is missing "enabled" column. Run "rails g stitches:add_enabled_to_api_clients"')
|
|
40
|
-
client = ApiClient.where(key: key).first
|
|
41
|
-
end
|
|
42
|
-
|
|
30
|
+
client = Stitches::ApiClientAccessWrapper.fetch_for_key(key)
|
|
43
31
|
if client.present?
|
|
44
32
|
env[@configuration.env_var_to_hold_api_client_primary_key] = client.id
|
|
45
33
|
env[@configuration.env_var_to_hold_api_client] = client
|
|
46
34
|
@app.call(env)
|
|
47
35
|
else
|
|
48
|
-
|
|
36
|
+
unauthorized_response("key invalid")
|
|
49
37
|
end
|
|
50
38
|
else
|
|
51
|
-
|
|
39
|
+
unauthorized_response("bad authorization type")
|
|
52
40
|
end
|
|
53
41
|
else
|
|
54
|
-
|
|
42
|
+
unauthorized_response("no authorization header")
|
|
55
43
|
end
|
|
56
44
|
end
|
|
57
45
|
|
|
58
46
|
private
|
|
59
47
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
48
|
+
# TODO: (jdlubrano)
|
|
49
|
+
# Once Rails 5 support is no longer necessary, we can simply call
|
|
50
|
+
# Rails.application.class.module_parent. The module_parent method
|
|
51
|
+
# does not exist in Rails <= 5, though, so we need to gracefully fallback
|
|
52
|
+
# Rails.application.class.parent for Rails versions predating Rails 6.0.0.
|
|
53
|
+
def rails_app_module
|
|
54
|
+
application_class = Rails.application.class
|
|
55
|
+
parent = application_class.try(:module_parent) || application_class.parent
|
|
56
|
+
parent.to_s
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def unauthorized_response(reason)
|
|
60
|
+
status = 401
|
|
61
|
+
body = "Unauthorized - #{reason}"
|
|
62
|
+
header = { "WWW-Authenticate" => "#{@configuration.custom_http_auth_scheme} realm=#{rails_app_module}" }
|
|
63
|
+
Rack::Response.new(body, status, header).finish
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
end
|
|
@@ -13,6 +13,8 @@ class Stitches::Configuration
|
|
|
13
13
|
@custom_http_auth_scheme = UnsetString.new("custom_http_auth_scheme")
|
|
14
14
|
@env_var_to_hold_api_client_primary_key = NonNullString.new("env_var_to_hold_api_client_primary_key","STITCHES_API_CLIENT_ID")
|
|
15
15
|
@env_var_to_hold_api_client= NonNullString.new("env_var_to_hold_api_client","STITCHES_API_CLIENT")
|
|
16
|
+
@max_cache_ttl = NonNullInteger.new("max_cache_ttl", 0)
|
|
17
|
+
@max_cache_size = NonNullInteger.new("max_cache_size", 0)
|
|
16
18
|
end
|
|
17
19
|
|
|
18
20
|
# A RegExp that allows URLS around the mime type and api key requirements.
|
|
@@ -39,7 +41,7 @@ class Stitches::Configuration
|
|
|
39
41
|
@custom_http_auth_scheme = NonNullString.new("custom_http_auth_scheme",new_custom_http_auth_scheme)
|
|
40
42
|
end
|
|
41
43
|
|
|
42
|
-
# The name of the environment variable that the ApiKey middleware should use to
|
|
44
|
+
# The name of the environment variable that the ApiKey middleware should use to
|
|
43
45
|
# place the primary key of the authenticated ApiKey. For example, if a user provides
|
|
44
46
|
# the api key 1234-1234-1234-1234, and that maps to the primary key 42 in your database,
|
|
45
47
|
# the environment will contain "42" in the key provided here.
|
|
@@ -59,8 +61,40 @@ class Stitches::Configuration
|
|
|
59
61
|
@env_var_to_hold_api_client= NonNullString.new("env_var_to_hold_api_client",new_env_var_to_hold_api_client)
|
|
60
62
|
end
|
|
61
63
|
|
|
64
|
+
def max_cache_ttl
|
|
65
|
+
@max_cache_ttl.to_i
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def max_cache_ttl=(new_max_cache_ttl)
|
|
69
|
+
@max_cache_ttl = NonNullInteger.new("max_cache_ttl", new_max_cache_ttl)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def max_cache_size
|
|
73
|
+
@max_cache_size.to_i
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def max_cache_size=(new_max_cache_size)
|
|
77
|
+
@max_cache_size = NonNullInteger.new("max_cache_size", new_max_cache_size)
|
|
78
|
+
end
|
|
79
|
+
|
|
62
80
|
private
|
|
63
81
|
|
|
82
|
+
class NonNullInteger
|
|
83
|
+
def initialize(name, value)
|
|
84
|
+
unless value.is_a?(Integer)
|
|
85
|
+
raise "#{name} must be an Integer, not a #{value.class}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
@value = value
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def to_i
|
|
92
|
+
@value
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
alias to_integer to_i
|
|
96
|
+
end
|
|
97
|
+
|
|
64
98
|
class NonNullString
|
|
65
99
|
def initialize(name,string)
|
|
66
100
|
unless string.nil? || string.is_a?(String)
|
|
@@ -11,4 +11,14 @@ Stitches.configure do |configuration|
|
|
|
11
11
|
# Env var that gets the primary key of the authenticated ApiKey
|
|
12
12
|
# for access in your controllers, so they don't need to re-parse the header
|
|
13
13
|
# configuration.env_var_to_hold_api_client_primary_key = "YOUR_ENV_VAR"
|
|
14
|
+
|
|
15
|
+
# Configures how long to cache ApiKeys in memory (In Seconds)
|
|
16
|
+
# A value of 0 will disable the cache entierly
|
|
17
|
+
# Default is 0
|
|
18
|
+
# configuration.max_cache_ttl = 5
|
|
19
|
+
|
|
20
|
+
# Configures how many ApiKeys to cache at one time
|
|
21
|
+
# This should be larger then the number of clients
|
|
22
|
+
# Default is 0
|
|
23
|
+
# configuration.max_cache_size = 100
|
|
14
24
|
end
|
|
@@ -123,14 +123,22 @@ feature "general API stuff" do
|
|
|
123
123
|
failure_message do
|
|
124
124
|
correct_code,_ = evaluate_response(response)
|
|
125
125
|
if correct_code
|
|
126
|
-
"Expected WWW-Authenticate header to be 'CustomKeyAuth realm=#{
|
|
126
|
+
"Expected WWW-Authenticate header to be 'CustomKeyAuth realm=#{realm}', but was #{response['WWW-Authenticate']}"
|
|
127
127
|
else
|
|
128
128
|
"Expected response to be 401, but was #{response.response_code}"
|
|
129
129
|
end
|
|
130
130
|
end
|
|
131
131
|
|
|
132
|
+
def realm
|
|
133
|
+
<% if ::Rails::VERSION::MAJOR >= 6 -%>
|
|
134
|
+
Rails.application.class.module_parent.to_s
|
|
135
|
+
<% else %>
|
|
136
|
+
Rails.application.class.parent.to_s
|
|
137
|
+
<% end %>
|
|
138
|
+
end
|
|
139
|
+
|
|
132
140
|
def evaluate_response(response)
|
|
133
|
-
[response.response_code == 401, response.headers["WWW-Authenticate"] == "CustomKeyAuth realm=#{
|
|
141
|
+
[response.response_code == 401, response.headers["WWW-Authenticate"] == "CustomKeyAuth realm=#{realm}"]
|
|
134
142
|
end
|
|
135
143
|
end
|
|
136
144
|
end
|
data/lib/stitches/railtie.rb
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
require 'stitches/api_key'
|
|
2
2
|
require 'stitches/valid_mime_type'
|
|
3
|
+
require 'stitches/api_client_access_wrapper'
|
|
3
4
|
|
|
4
5
|
module Stitches
|
|
5
6
|
class Railtie < Rails::Railtie
|
|
6
7
|
config.app_middleware.use Stitches::ApiKey
|
|
7
8
|
config.app_middleware.use Stitches::ValidMimeType
|
|
9
|
+
|
|
8
10
|
end
|
|
9
11
|
end
|
|
@@ -16,16 +16,17 @@ module Stitches
|
|
|
16
16
|
if accept =~ %r{application/json} && accept =~ %r{version=\d+}
|
|
17
17
|
@app.call(env)
|
|
18
18
|
else
|
|
19
|
-
|
|
19
|
+
not_acceptable_response(accept)
|
|
20
20
|
end
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
private
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
def not_acceptable_response(accept_header)
|
|
26
|
+
status = 406
|
|
27
|
+
body = "Not Acceptable - '#{accept_header}' didn't have the right mime type or version number. We only accept application/json with a version"
|
|
28
|
+
header = { "WWW-Authenticate" => accept_header }
|
|
29
|
+
Rack::Response.new(body, status, header).finish
|
|
29
30
|
end
|
|
30
31
|
|
|
31
32
|
end
|
data/lib/stitches/version.rb
CHANGED
data/owners.json
CHANGED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
require 'spec_helper.rb'
|
|
2
|
+
|
|
3
|
+
module MyApp
|
|
4
|
+
class Application
|
|
5
|
+
end
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
unless defined? ApiClient
|
|
9
|
+
class ApiClient
|
|
10
|
+
def self.column_names
|
|
11
|
+
["enabled"]
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
describe Stitches::ApiClientAccessWrapper do
|
|
17
|
+
let(:api_client) {
|
|
18
|
+
double(ApiClient, id: 42)
|
|
19
|
+
}
|
|
20
|
+
before do
|
|
21
|
+
Stitches.configuration.reset_to_defaults!
|
|
22
|
+
end
|
|
23
|
+
describe '#fetch_by_key' do
|
|
24
|
+
context "cache is disabled" do
|
|
25
|
+
before do
|
|
26
|
+
expect(ApiClient).to receive(:find_by).and_return(api_client).twice
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "fetchs object from db twice" do
|
|
30
|
+
expect(described_class.fetch_for_key("123").id).to eq(42)
|
|
31
|
+
expect(described_class.fetch_for_key("123").id).to eq(42)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
context "cache is configured" do
|
|
36
|
+
before do
|
|
37
|
+
Stitches.configure do |config|
|
|
38
|
+
config.max_cache_ttl = 5
|
|
39
|
+
config.max_cache_size = 10
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
expect(ApiClient).to receive(:find_by).and_return(api_client).once
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it "fetchs object from cache" do
|
|
46
|
+
expect(described_class.fetch_for_key("123").id).to eq(42)
|
|
47
|
+
# This should hit the cache
|
|
48
|
+
expect(described_class.fetch_for_key("123").id).to eq(42)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
data/spec/api_key_spec.rb
CHANGED
|
@@ -15,10 +15,8 @@ end
|
|
|
15
15
|
|
|
16
16
|
describe Stitches::ApiKey do
|
|
17
17
|
let(:app) { double("rack app") }
|
|
18
|
-
let(:
|
|
19
|
-
|
|
20
|
-
double(ApiClient, id: 42)
|
|
21
|
-
]
|
|
18
|
+
let(:api_client) {
|
|
19
|
+
double(ApiClient, id: 42)
|
|
22
20
|
}
|
|
23
21
|
|
|
24
22
|
before do
|
|
@@ -27,23 +25,27 @@ describe Stitches::ApiKey do
|
|
|
27
25
|
fake_rails_app = MyApp::Application.new
|
|
28
26
|
allow(Rails).to receive(:application).and_return(fake_rails_app)
|
|
29
27
|
allow(app).to receive(:call).with(env)
|
|
30
|
-
allow(ApiClient).to receive(:
|
|
28
|
+
allow(ApiClient).to receive(:find_by).and_return(api_client)
|
|
29
|
+
Stitches::ApiClientAccessWrapper.clear_api_cache
|
|
31
30
|
end
|
|
32
31
|
|
|
33
32
|
subject(:middleware) { described_class.new(app, namespace: "/api") }
|
|
34
33
|
|
|
35
34
|
shared_examples "an unauthorized response" do
|
|
36
35
|
it "returns a 401" do
|
|
37
|
-
|
|
36
|
+
status, _headers, _body = @response
|
|
37
|
+
expect(status).to eq(401)
|
|
38
38
|
end
|
|
39
39
|
it "sets the proper header" do
|
|
40
|
-
|
|
40
|
+
_status, headers, _body = @response
|
|
41
|
+
expect(headers["WWW-Authenticate"]).to eq("MyAwesomeInternalScheme realm=MyApp")
|
|
41
42
|
end
|
|
42
43
|
it "stops the call chain preventing anything from happening" do
|
|
43
44
|
expect(app).not_to have_received(:call)
|
|
44
45
|
end
|
|
45
46
|
it "sends a reasonable message" do
|
|
46
|
-
|
|
47
|
+
_status, _headers, body = @response
|
|
48
|
+
expect(body).to eq([expected_body])
|
|
47
49
|
end
|
|
48
50
|
end
|
|
49
51
|
|
|
@@ -155,18 +157,17 @@ describe Stitches::ApiKey do
|
|
|
155
157
|
end
|
|
156
158
|
|
|
157
159
|
it "sets the api_client's ID in the environment" do
|
|
158
|
-
expect(env[Stitches.configuration.env_var_to_hold_api_client_primary_key]).to eq(
|
|
160
|
+
expect(env[Stitches.configuration.env_var_to_hold_api_client_primary_key]).to eq(api_client.id)
|
|
159
161
|
end
|
|
160
162
|
|
|
161
163
|
it "sets the api_client itself in the environment" do
|
|
162
|
-
expect(env[Stitches.configuration.env_var_to_hold_api_client]).to eq(
|
|
164
|
+
expect(env[Stitches.configuration.env_var_to_hold_api_client]).to eq(api_client)
|
|
163
165
|
end
|
|
164
166
|
end
|
|
165
167
|
|
|
166
168
|
context "unauthorized responses" do
|
|
167
169
|
before do
|
|
168
170
|
@response = middleware.call(env)
|
|
169
|
-
@response.finish
|
|
170
171
|
end
|
|
171
172
|
context "invalid key" do
|
|
172
173
|
let(:env) {
|
|
@@ -175,7 +176,7 @@ describe Stitches::ApiKey do
|
|
|
175
176
|
"HTTP_AUTHORIZATION" => "MyAwesomeInternalScheme key=foobar",
|
|
176
177
|
}
|
|
177
178
|
}
|
|
178
|
-
let(:
|
|
179
|
+
let(:api_client) { nil }
|
|
179
180
|
|
|
180
181
|
it_behaves_like "an unauthorized response" do
|
|
181
182
|
let(:expected_body) { "Unauthorized - key invalid" }
|
data/spec/configuration_spec.rb
CHANGED
|
@@ -9,17 +9,23 @@ describe Stitches::Configuration do
|
|
|
9
9
|
let(:allowlist_regexp) { %r{foo} }
|
|
10
10
|
let(:custom_http_auth_scheme) { "Blah" }
|
|
11
11
|
let(:env_var_to_hold_api_client_primary_key) { "FOOBAR" }
|
|
12
|
+
let(:max_cache_ttl) { 11 }
|
|
13
|
+
let(:max_cache_size) { 111 }
|
|
12
14
|
|
|
13
15
|
it "can be configured globally" do
|
|
14
16
|
Stitches.configure do |config|
|
|
15
17
|
config.allowlist_regexp = allowlist_regexp
|
|
16
18
|
config.custom_http_auth_scheme = custom_http_auth_scheme
|
|
17
19
|
config.env_var_to_hold_api_client_primary_key = env_var_to_hold_api_client_primary_key
|
|
20
|
+
config.max_cache_ttl = max_cache_ttl
|
|
21
|
+
config.max_cache_size = max_cache_size
|
|
18
22
|
end
|
|
19
23
|
|
|
20
24
|
expect(Stitches.configuration.allowlist_regexp).to eq(allowlist_regexp)
|
|
21
25
|
expect(Stitches.configuration.custom_http_auth_scheme).to eq(custom_http_auth_scheme)
|
|
22
26
|
expect(Stitches.configuration.env_var_to_hold_api_client_primary_key).to eq(env_var_to_hold_api_client_primary_key)
|
|
27
|
+
expect(Stitches.configuration.max_cache_ttl).to eq(max_cache_ttl)
|
|
28
|
+
expect(Stitches.configuration.max_cache_size).to eq(max_cache_size)
|
|
23
29
|
end
|
|
24
30
|
|
|
25
31
|
it "defaults to nil for allowlist_regexp" do
|
|
@@ -30,6 +36,14 @@ describe Stitches::Configuration do
|
|
|
30
36
|
expect(Stitches.configuration.env_var_to_hold_api_client_primary_key).to eq("STITCHES_API_CLIENT_ID")
|
|
31
37
|
end
|
|
32
38
|
|
|
39
|
+
it "defaults to 0 for max_cache_ttl" do
|
|
40
|
+
expect(Stitches.configuration.max_cache_ttl).to eq(0)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "sets a default for max_cache_size" do
|
|
44
|
+
expect(Stitches.configuration.max_cache_size).to eq(0)
|
|
45
|
+
end
|
|
46
|
+
|
|
33
47
|
it "blows up if you try to use custom_http_auth_scheme without having set it" do
|
|
34
48
|
expect {
|
|
35
49
|
Stitches.configuration.custom_http_auth_scheme
|
|
@@ -102,6 +116,37 @@ describe Stitches::Configuration do
|
|
|
102
116
|
}.not_to raise_error
|
|
103
117
|
end
|
|
104
118
|
end
|
|
119
|
+
|
|
120
|
+
describe "max_cache_ttl" do
|
|
121
|
+
let(:config) { Stitches::Configuration.new }
|
|
122
|
+
it "must be an integer" do
|
|
123
|
+
expect {
|
|
124
|
+
config.max_cache_ttl = ""
|
|
125
|
+
}.to raise_error(/max_cache_ttl must be an Integer, not a String/)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it "may not be nil" do
|
|
129
|
+
expect {
|
|
130
|
+
config.max_cache_ttl = nil
|
|
131
|
+
}.to raise_error(/max_cache_ttl must be an Integer, not a NilClass/)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
describe "max_cache_size" do
|
|
136
|
+
let(:config) { Stitches::Configuration.new }
|
|
137
|
+
it "must be an integer" do
|
|
138
|
+
expect {
|
|
139
|
+
config.max_cache_size = ""
|
|
140
|
+
}.to raise_error(/max_cache_size must be an Integer, not a String/)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it "may not be nil" do
|
|
144
|
+
expect {
|
|
145
|
+
config.max_cache_size = nil
|
|
146
|
+
}.to raise_error(/max_cache_size must be an Integer, not a NilClass/)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
105
150
|
context "deprecated options we want to support for backwards compatibility" do
|
|
106
151
|
|
|
107
152
|
let(:logger) { double("logger") }
|
|
@@ -5,9 +5,10 @@ require "open3"
|
|
|
5
5
|
RSpec.describe "Adding Stitches to a New Rails App", :integration do
|
|
6
6
|
let(:work_dir) { Dir.mktmpdir }
|
|
7
7
|
let(:rails_app_name) { "swamp-thing" }
|
|
8
|
+
let(:rails_root) { Pathname(work_dir) / rails_app_name }
|
|
8
9
|
|
|
9
10
|
def run(command)
|
|
10
|
-
stdout, stderr, stat = Open3.capture3(command)
|
|
11
|
+
stdout, stderr, stat = Open3.capture3({ 'BUNDLE_GEMFILE' => rails_root.join('Gemfile').to_path }, command)
|
|
11
12
|
success = stat.success? && stdout !~ /Could not find generator/im
|
|
12
13
|
|
|
13
14
|
if ENV["DEBUG"] == 'true' || !success
|
|
@@ -37,21 +38,30 @@ RSpec.describe "Adding Stitches to a New Rails App", :integration do
|
|
|
37
38
|
"--no-rc",
|
|
38
39
|
"--skip-bundle",
|
|
39
40
|
].join(" ")
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
|
|
42
|
+
# Use this local version of stitches rather than the one on Rubygems
|
|
43
|
+
gem_path = File.expand_path("../..", File.dirname(__FILE__))
|
|
44
|
+
use_local_stitches = %{echo "gem 'stitches', path: '#{gem_path}'" >> Gemfile}
|
|
45
|
+
|
|
46
|
+
Bundler.with_clean_env do
|
|
47
|
+
FileUtils.chdir work_dir do
|
|
48
|
+
run rails_new
|
|
49
|
+
|
|
50
|
+
FileUtils.chdir rails_app_name do
|
|
51
|
+
run use_local_stitches
|
|
52
|
+
# It's unclear why, but on CI the gems are not found when installed
|
|
53
|
+
# through bundler however installing them explicitly first fixes it.
|
|
54
|
+
run "gem install apitome rspec-rails rspec_api_documentation"
|
|
55
|
+
run "bundle install"
|
|
56
|
+
example.run
|
|
57
|
+
end
|
|
44
58
|
end
|
|
45
59
|
end
|
|
46
60
|
end
|
|
47
61
|
|
|
48
62
|
it "works as described in the README" do
|
|
49
|
-
run "bin/rails generate rspec:install"
|
|
50
|
-
run "bin/rails generate apitome:install"
|
|
51
63
|
run "bin/rails generate stitches:api"
|
|
52
64
|
|
|
53
|
-
rails_root = Pathname(work_dir) / rails_app_name
|
|
54
|
-
|
|
55
65
|
# Yuck! So much duplication! BUT: Rails app templates have a notoriously silent failure mode, so mostly
|
|
56
66
|
# what this is doing is ensuring that the generator inserted stuff when asked and that the very basics of what happens
|
|
57
67
|
# during generation are there. It's gross, and I'm sorry.
|
|
@@ -60,9 +70,7 @@ RSpec.describe "Adding Stitches to a New Rails App", :integration do
|
|
|
60
70
|
aggregate_failures do
|
|
61
71
|
expect(File.exist?(rails_root / "app" / "controllers" / "api" / "api_controller.rb")).to eq(true)
|
|
62
72
|
expect(rails_root / "Gemfile").to contain_gem("apitome")
|
|
63
|
-
expect(rails_root / "Gemfile").to contain_gem("responders")
|
|
64
73
|
expect(rails_root / "Gemfile").to contain_gem("rspec_api_documentation")
|
|
65
|
-
expect(rails_root / "Gemfile").to contain_gem("capybara")
|
|
66
74
|
expect(rails_root / "config" / "routes.rb").to have_route(namespace: :api, module_scope: :v1, resource: 'ping')
|
|
67
75
|
expect(rails_root / "config" / "routes.rb").to have_route(namespace: :api, module_scope: :v2, resource: 'ping')
|
|
68
76
|
expect(rails_root / "config" / "routes.rb").to have_mounted_engine("Apitome::Engine")
|
|
@@ -81,11 +89,7 @@ RSpec.describe "Adding Stitches to a New Rails App", :integration do
|
|
|
81
89
|
end
|
|
82
90
|
|
|
83
91
|
it "inserts the deprecation module into ApiController" do
|
|
84
|
-
run "bin/rails generate rspec:install"
|
|
85
|
-
run "bin/rails generate apitome:install"
|
|
86
92
|
run "bin/rails generate stitches:api"
|
|
87
|
-
|
|
88
|
-
rails_root = Pathname(work_dir) / rails_app_name
|
|
89
93
|
api_controller = rails_root / "app" / "controllers" / "api" / "api_controller.rb"
|
|
90
94
|
|
|
91
95
|
api_controller_contents = File.read(api_controller).split(/\n/)
|
|
@@ -106,11 +110,8 @@ RSpec.describe "Adding Stitches to a New Rails App", :integration do
|
|
|
106
110
|
end
|
|
107
111
|
|
|
108
112
|
it "inserts can update old configuration" do
|
|
109
|
-
run "bin/rails generate rspec:install"
|
|
110
|
-
run "bin/rails generate apitome:install"
|
|
111
113
|
run "bin/rails generate stitches:api"
|
|
112
114
|
|
|
113
|
-
rails_root = Pathname(work_dir) / rails_app_name
|
|
114
115
|
initializer = rails_root / "config" / "initializers" / "stitches.rb"
|
|
115
116
|
|
|
116
117
|
initializer_contents = File.read(initializer).split(/\n/)
|
|
@@ -11,13 +11,15 @@ describe Stitches::ValidMimeType do
|
|
|
11
11
|
|
|
12
12
|
shared_examples "an unacceptable response" do
|
|
13
13
|
it "returns a 406" do
|
|
14
|
-
|
|
14
|
+
status, _headers, _body = @response
|
|
15
|
+
expect(status).to eq(406)
|
|
15
16
|
end
|
|
16
17
|
it "stops the call chain preventing anything from happening" do
|
|
17
18
|
expect(app).not_to have_received(:call)
|
|
18
19
|
end
|
|
19
20
|
it "sends a reasonable message" do
|
|
20
|
-
|
|
21
|
+
_status, _headers, body = @response
|
|
22
|
+
expect(body.first).to match(/didn't have the right mime type or version number. We only accept application\/json/)
|
|
21
23
|
end
|
|
22
24
|
end
|
|
23
25
|
|
|
@@ -133,7 +135,6 @@ describe Stitches::ValidMimeType do
|
|
|
133
135
|
context "unacceptable responses" do
|
|
134
136
|
before do
|
|
135
137
|
@response = middleware.call(env)
|
|
136
|
-
@response.finish
|
|
137
138
|
end
|
|
138
139
|
context "no header" do
|
|
139
140
|
let(:env) {
|
data/stitches.gemspec
CHANGED
|
@@ -20,10 +20,9 @@ Gem::Specification.new do |s|
|
|
|
20
20
|
|
|
21
21
|
s.add_runtime_dependency("rails")
|
|
22
22
|
s.add_runtime_dependency("pg")
|
|
23
|
-
s.add_runtime_dependency("
|
|
24
|
-
s.add_runtime_dependency("rspec-rails", "~> 3")
|
|
25
|
-
s.add_runtime_dependency("apitome")
|
|
23
|
+
s.add_runtime_dependency("lru_redux")
|
|
26
24
|
|
|
25
|
+
s.add_development_dependency("rspec", ">= 3")
|
|
27
26
|
s.add_development_dependency("rake")
|
|
28
27
|
s.add_development_dependency("rspec_junit_formatter")
|
|
29
28
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: stitches
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 4.0.0.RC1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stitch Fix Engineering
|
|
@@ -11,7 +11,7 @@ authors:
|
|
|
11
11
|
autorequire:
|
|
12
12
|
bindir: bin
|
|
13
13
|
cert_chain: []
|
|
14
|
-
date:
|
|
14
|
+
date: 2020-07-22 00:00:00.000000000 Z
|
|
15
15
|
dependencies:
|
|
16
16
|
- !ruby/object:Gem::Dependency
|
|
17
17
|
name: rails
|
|
@@ -42,47 +42,33 @@ dependencies:
|
|
|
42
42
|
- !ruby/object:Gem::Version
|
|
43
43
|
version: '0'
|
|
44
44
|
- !ruby/object:Gem::Dependency
|
|
45
|
-
name:
|
|
45
|
+
name: lru_redux
|
|
46
46
|
requirement: !ruby/object:Gem::Requirement
|
|
47
47
|
requirements:
|
|
48
48
|
- - ">="
|
|
49
49
|
- !ruby/object:Gem::Version
|
|
50
|
-
version: '
|
|
50
|
+
version: '0'
|
|
51
51
|
type: :runtime
|
|
52
52
|
prerelease: false
|
|
53
53
|
version_requirements: !ruby/object:Gem::Requirement
|
|
54
54
|
requirements:
|
|
55
55
|
- - ">="
|
|
56
56
|
- !ruby/object:Gem::Version
|
|
57
|
-
version: '
|
|
58
|
-
- !ruby/object:Gem::Dependency
|
|
59
|
-
name: rspec-rails
|
|
60
|
-
requirement: !ruby/object:Gem::Requirement
|
|
61
|
-
requirements:
|
|
62
|
-
- - "~>"
|
|
63
|
-
- !ruby/object:Gem::Version
|
|
64
|
-
version: '3'
|
|
65
|
-
type: :runtime
|
|
66
|
-
prerelease: false
|
|
67
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
68
|
-
requirements:
|
|
69
|
-
- - "~>"
|
|
70
|
-
- !ruby/object:Gem::Version
|
|
71
|
-
version: '3'
|
|
57
|
+
version: '0'
|
|
72
58
|
- !ruby/object:Gem::Dependency
|
|
73
|
-
name:
|
|
59
|
+
name: rspec
|
|
74
60
|
requirement: !ruby/object:Gem::Requirement
|
|
75
61
|
requirements:
|
|
76
62
|
- - ">="
|
|
77
63
|
- !ruby/object:Gem::Version
|
|
78
|
-
version: '
|
|
79
|
-
type: :
|
|
64
|
+
version: '3'
|
|
65
|
+
type: :development
|
|
80
66
|
prerelease: false
|
|
81
67
|
version_requirements: !ruby/object:Gem::Requirement
|
|
82
68
|
requirements:
|
|
83
69
|
- - ">="
|
|
84
70
|
- !ruby/object:Gem::Version
|
|
85
|
-
version: '
|
|
71
|
+
version: '3'
|
|
86
72
|
- !ruby/object:Gem::Dependency
|
|
87
73
|
name: rake
|
|
88
74
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -123,11 +109,12 @@ extensions: []
|
|
|
123
109
|
extra_rdoc_files: []
|
|
124
110
|
files:
|
|
125
111
|
- ".circleci/config.yml"
|
|
112
|
+
- ".github/CODEOWNERS"
|
|
113
|
+
- ".github/PULL_REQUEST_TEMPLATE.md"
|
|
126
114
|
- ".gitignore"
|
|
127
115
|
- ".ruby-gemset"
|
|
128
116
|
- ".ruby-version"
|
|
129
117
|
- ".travis.yml"
|
|
130
|
-
- CODEOWNERS
|
|
131
118
|
- CODE_OF_CONDUCT.md
|
|
132
119
|
- CONTRIBUTING.md
|
|
133
120
|
- Gemfile
|
|
@@ -135,8 +122,8 @@ files:
|
|
|
135
122
|
- Gemfile.rails-5.0
|
|
136
123
|
- Gemfile.rails-5.1
|
|
137
124
|
- Gemfile.rails-5.2
|
|
125
|
+
- Gemfile.rails-6.0
|
|
138
126
|
- LICENSE.txt
|
|
139
|
-
- PULL_REQUEST_TEMPLATE.md
|
|
140
127
|
- README.md
|
|
141
128
|
- Rakefile
|
|
142
129
|
- build-matrix.json
|
|
@@ -144,6 +131,7 @@ files:
|
|
|
144
131
|
- lib/stitches/add_deprecation_generator.rb
|
|
145
132
|
- lib/stitches/add_enabled_to_api_clients_generator.rb
|
|
146
133
|
- lib/stitches/allowlist_middleware.rb
|
|
134
|
+
- lib/stitches/api_client_access_wrapper.rb
|
|
147
135
|
- lib/stitches/api_generator.rb
|
|
148
136
|
- lib/stitches/api_key.rb
|
|
149
137
|
- lib/stitches/api_version_constraint.rb
|
|
@@ -180,6 +168,7 @@ files:
|
|
|
180
168
|
- lib/stitches/whitelisting_middleware.rb
|
|
181
169
|
- lib/stitches_norailtie.rb
|
|
182
170
|
- owners.json
|
|
171
|
+
- spec/api_client_access_wrapper_spec.rb
|
|
183
172
|
- spec/api_key_spec.rb
|
|
184
173
|
- spec/api_version_constraint_spec.rb
|
|
185
174
|
- spec/configuration_spec.rb
|
|
@@ -208,16 +197,16 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
208
197
|
version: '0'
|
|
209
198
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
210
199
|
requirements:
|
|
211
|
-
- - "
|
|
200
|
+
- - ">"
|
|
212
201
|
- !ruby/object:Gem::Version
|
|
213
|
-
version:
|
|
202
|
+
version: 1.3.1
|
|
214
203
|
requirements: []
|
|
215
|
-
|
|
216
|
-
rubygems_version: 2.7.6
|
|
204
|
+
rubygems_version: 3.1.2
|
|
217
205
|
signing_key:
|
|
218
206
|
specification_version: 4
|
|
219
207
|
summary: You'll be in stitches at how easy it is to create a service at Stitch Fix
|
|
220
208
|
test_files:
|
|
209
|
+
- spec/api_client_access_wrapper_spec.rb
|
|
221
210
|
- spec/api_key_spec.rb
|
|
222
211
|
- spec/api_version_constraint_spec.rb
|
|
223
212
|
- spec/configuration_spec.rb
|
data/PULL_REQUEST_TEMPLATE.md
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
## Problem
|
|
2
|
-
|
|
3
|
-
«Brief overview of the problem»
|
|
4
|
-
|
|
5
|
-
## Solution
|
|
6
|
-
|
|
7
|
-
«Brief description of how you solved the problem»
|
|
8
|
-
|
|
9
|
-
## Checklist
|
|
10
|
-
|
|
11
|
-
### Before Merging
|
|
12
|
-
|
|
13
|
-
- [ ] If there is an RC on this branch, revert the version change in `version.rb`
|
|
14
|
-
|
|
15
|
-
### After Merging
|
|
16
|
-
|
|
17
|
-
See the [gem release process](https://github.com/stitchfix/eng-wiki/blob/master/technical-topics/updating-gem-versions.md) for a detailed list, but the gist of it is:
|
|
18
|
-
|
|
19
|
-
- [ ] Fetch `master` locally and run the applicable `rake version:*` task **on `master`** to bump the version
|
|
20
|
-
- [ ] Run `rake release` **on `master`** to release the new version on Gemfury
|
|
21
|
-
- [ ] Add [release notes](https://github.com/stitchfix/messaging/releases) - **this is very important in helping other engineers understand what changed in the new version**
|