stitches 3.8.2 → 4.1.0RC2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ad83ebbf3aed8767a280f814d64be8819dd200586bd737de0cdb402516539476
4
- data.tar.gz: fba83f42359be56a03517351dab3bb877bf9fc50d7acfb6de6c48117544472e3
3
+ metadata.gz: 181e2199d9e7f15ef933d3c3b87262f83894e02c25821d53aa23191db16c6a7a
4
+ data.tar.gz: 8f90ed0f8e39e94715f77f2708f1fe87da31318f97708dfd50692f8408ec9d71
5
5
  SHA512:
6
- metadata.gz: 5288beef3b5831fbade2a210b5b18c404ceae447833e35303c4cdc041888c0fbd8a71e2d343b10387316c5bfa2b13d04a3727c5b83fcf3114cf7b2c26a7a043b
7
- data.tar.gz: 0fc6d6ba8044ba4df0fcd57826c6028926158a0201b96c27122e66d73070288a148bfee47d8352345f4e7840dd85531182ced2f933c39f1b10dc398e3c83d70d
6
+ metadata.gz: f4043849fc0c7da16cd1a87216897a6bf696b0cf82adf18d5a2d0e0277fd8ba7881e6af75e1c6451ee9fece50902bb1088166ac46c40eddc74cc4b3c01f5ecc5
7
+ data.tar.gz: 2e2310a95b713fe859ce309cac4d03552fd094ac03d2526045095980f488204a27a04da5c459b90719af9f947af13f1d70301b9eddb21f90a742470198760bd2
data/.circleci/config.yml CHANGED
@@ -3,9 +3,33 @@
3
3
  ---
4
4
  version: 2
5
5
  jobs:
6
+ generate-and-push-docs:
7
+ docker:
8
+ - image: circleci/ruby:3.0.0
9
+ auth:
10
+ username: "$DOCKERHUB_USERNAME"
11
+ password: "$DOCKERHUB_PASSWORD"
12
+ steps:
13
+ - checkout
14
+ - run: bundle config stitchfix01.jfrog.io $ARTIFACTORY_USER:$ARTIFACTORY_TOKEN
15
+ - run: bundle install --full-index
16
+ - run:
17
+ name: Generate documentation
18
+ command: ' if [[ $(bundle exec rake -T docs:generate:custom) ]]; then echo
19
+ "Generating docs using rake task docs:generate:custom" ; bundle exec rake
20
+ docs:generate:custom ; elif [[ $(bundle exec rake -T docs:generate) ]];
21
+ then echo "Generating docs using rake task docs:generate" ; bundle exec
22
+ rake docs:generate ; else echo "Skipping doc generation" ; exit 0 ; fi '
23
+ - run:
24
+ name: Push documentation to Unwritten
25
+ command: if [[ $(bundle exec rake -T docs:push) ]]; then bundle exec rake
26
+ docs:push; fi
6
27
  release:
7
28
  docker:
8
- - image: circleci/ruby:2.7.0
29
+ - image: circleci/ruby:3.0.0
30
+ auth:
31
+ username: "$DOCKERHUB_USERNAME"
32
+ password: "$DOCKERHUB_PASSWORD"
9
33
  steps:
10
34
  - checkout
11
35
  - run: bundle config stitchfix01.jfrog.io $ARTIFACTORY_USER:$ARTIFACTORY_TOKEN
@@ -17,14 +41,52 @@ jobs:
17
41
  - run:
18
42
  name: Build/release gem to artifactory
19
43
  command: bundle exec rake push_artifactory
20
- ruby-2.7.0-rails-6.0:
44
+ ruby-3.0.0-rails-6.1:
21
45
  docker:
22
- - image: circleci/ruby:2.7.0
46
+ - image: circleci/ruby:3.0.0
47
+ auth:
48
+ username: "$DOCKERHUB_USERNAME"
49
+ password: "$DOCKERHUB_PASSWORD"
23
50
  environment:
24
- BUNDLE_GEMFILE: Gemfile.rails-6.0
51
+ BUNDLE_GEMFILE: Gemfile.rails-6.1
52
+ working_directory: "~/stitches"
53
+ steps:
54
+ - checkout
55
+ - run:
56
+ name: Check for Gemfile.lock presence
57
+ command: ' if (test -f Gemfile.lock) then echo "Dont commit Gemfile.lock (see
58
+ https://github.com/stitchfix/eng-wiki/blob/master/architecture-decisions/0009-rubygem-dependencies-will-be-managed-more-explicitly.md)"
59
+ 1>&2 ; exit 1 ; else exit 0 ; fi '
60
+ - run: bundle config stitchfix01.jfrog.io $ARTIFACTORY_USER:$ARTIFACTORY_TOKEN
61
+ - run: bundle install --full-index
62
+ - run: bundle exec rspec --format RspecJunitFormatter --out /tmp/test-results/rspec.xml
63
+ --format=doc
64
+ - run:
65
+ name: Run Additional CI Steps
66
+ command: if [ -e bin/additional-ci-steps ]; then bin/additional-ci-steps;
67
+ fi
68
+ - run:
69
+ name: Notify Pager Duty
70
+ command: bundle exec y-notify "#eng-runtime-alerts"
71
+ when: on_fail
72
+ - store_test_results:
73
+ path: "/tmp/test-results"
74
+ ruby-2.7.2-rails-6.1:
75
+ docker:
76
+ - image: circleci/ruby:2.7.2
77
+ auth:
78
+ username: "$DOCKERHUB_USERNAME"
79
+ password: "$DOCKERHUB_PASSWORD"
80
+ environment:
81
+ BUNDLE_GEMFILE: Gemfile.rails-6.1
25
82
  working_directory: "~/stitches"
26
83
  steps:
27
84
  - checkout
85
+ - run:
86
+ name: Check for Gemfile.lock presence
87
+ command: ' if (test -f Gemfile.lock) then echo "Dont commit Gemfile.lock (see
88
+ https://github.com/stitchfix/eng-wiki/blob/master/architecture-decisions/0009-rubygem-dependencies-will-be-managed-more-explicitly.md)"
89
+ 1>&2 ; exit 1 ; else exit 0 ; fi '
28
90
  - run: bundle config stitchfix01.jfrog.io $ARTIFACTORY_USER:$ARTIFACTORY_TOKEN
29
91
  - run: bundle install --full-index
30
92
  - run: bundle exec rspec --format RspecJunitFormatter --out /tmp/test-results/rspec.xml
@@ -35,18 +97,26 @@ jobs:
35
97
  fi
36
98
  - run:
37
99
  name: Notify Pager Duty
38
- command: bundle exec y-notify "#devex-alerts"
100
+ command: bundle exec y-notify "#eng-runtime-alerts"
39
101
  when: on_fail
40
102
  - store_test_results:
41
103
  path: "/tmp/test-results"
42
- ruby-2.6.5-rails-6.0:
104
+ ruby-3.0.0-rails-6.0:
43
105
  docker:
44
- - image: circleci/ruby:2.6.5
106
+ - image: circleci/ruby:3.0.0
107
+ auth:
108
+ username: "$DOCKERHUB_USERNAME"
109
+ password: "$DOCKERHUB_PASSWORD"
45
110
  environment:
46
111
  BUNDLE_GEMFILE: Gemfile.rails-6.0
47
112
  working_directory: "~/stitches"
48
113
  steps:
49
114
  - checkout
115
+ - run:
116
+ name: Check for Gemfile.lock presence
117
+ command: ' if (test -f Gemfile.lock) then echo "Dont commit Gemfile.lock (see
118
+ https://github.com/stitchfix/eng-wiki/blob/master/architecture-decisions/0009-rubygem-dependencies-will-be-managed-more-explicitly.md)"
119
+ 1>&2 ; exit 1 ; else exit 0 ; fi '
50
120
  - run: bundle config stitchfix01.jfrog.io $ARTIFACTORY_USER:$ARTIFACTORY_TOKEN
51
121
  - run: bundle install --full-index
52
122
  - run: bundle exec rspec --format RspecJunitFormatter --out /tmp/test-results/rspec.xml
@@ -57,18 +127,26 @@ jobs:
57
127
  fi
58
128
  - run:
59
129
  name: Notify Pager Duty
60
- command: bundle exec y-notify "#devex-alerts"
130
+ command: bundle exec y-notify "#eng-runtime-alerts"
61
131
  when: on_fail
62
132
  - store_test_results:
63
133
  path: "/tmp/test-results"
64
- ruby-2.7.0-rails-5.2:
134
+ ruby-2.7.2-rails-6.0:
65
135
  docker:
66
- - image: circleci/ruby:2.7.0
136
+ - image: circleci/ruby:2.7.2
137
+ auth:
138
+ username: "$DOCKERHUB_USERNAME"
139
+ password: "$DOCKERHUB_PASSWORD"
67
140
  environment:
68
- BUNDLE_GEMFILE: Gemfile.rails-5.2
141
+ BUNDLE_GEMFILE: Gemfile.rails-6.0
69
142
  working_directory: "~/stitches"
70
143
  steps:
71
144
  - checkout
145
+ - run:
146
+ name: Check for Gemfile.lock presence
147
+ command: ' if (test -f Gemfile.lock) then echo "Dont commit Gemfile.lock (see
148
+ https://github.com/stitchfix/eng-wiki/blob/master/architecture-decisions/0009-rubygem-dependencies-will-be-managed-more-explicitly.md)"
149
+ 1>&2 ; exit 1 ; else exit 0 ; fi '
72
150
  - run: bundle config stitchfix01.jfrog.io $ARTIFACTORY_USER:$ARTIFACTORY_TOKEN
73
151
  - run: bundle install --full-index
74
152
  - run: bundle exec rspec --format RspecJunitFormatter --out /tmp/test-results/rspec.xml
@@ -79,18 +157,26 @@ jobs:
79
157
  fi
80
158
  - run:
81
159
  name: Notify Pager Duty
82
- command: bundle exec y-notify "#devex-alerts"
160
+ command: bundle exec y-notify "#eng-runtime-alerts"
83
161
  when: on_fail
84
162
  - store_test_results:
85
163
  path: "/tmp/test-results"
86
- ruby-2.6.5-rails-5.2:
164
+ ruby-2.7.2-rails-5.2:
87
165
  docker:
88
- - image: circleci/ruby:2.6.5
166
+ - image: circleci/ruby:2.7.2
167
+ auth:
168
+ username: "$DOCKERHUB_USERNAME"
169
+ password: "$DOCKERHUB_PASSWORD"
89
170
  environment:
90
171
  BUNDLE_GEMFILE: Gemfile.rails-5.2
91
172
  working_directory: "~/stitches"
92
173
  steps:
93
174
  - checkout
175
+ - run:
176
+ name: Check for Gemfile.lock presence
177
+ command: ' if (test -f Gemfile.lock) then echo "Dont commit Gemfile.lock (see
178
+ https://github.com/stitchfix/eng-wiki/blob/master/architecture-decisions/0009-rubygem-dependencies-will-be-managed-more-explicitly.md)"
179
+ 1>&2 ; exit 1 ; else exit 0 ; fi '
94
180
  - run: bundle config stitchfix01.jfrog.io $ARTIFACTORY_USER:$ARTIFACTORY_TOKEN
95
181
  - run: bundle install --full-index
96
182
  - run: bundle exec rspec --format RspecJunitFormatter --out /tmp/test-results/rspec.xml
@@ -101,7 +187,7 @@ jobs:
101
187
  fi
102
188
  - run:
103
189
  name: Notify Pager Duty
104
- command: bundle exec y-notify "#devex-alerts"
190
+ command: bundle exec y-notify "#eng-runtime-alerts"
105
191
  when: on_fail
106
192
  - store_test_results:
107
193
  path: "/tmp/test-results"
@@ -112,31 +198,46 @@ workflows:
112
198
  - release:
113
199
  context: org-global
114
200
  requires:
115
- - ruby-2.7.0-rails-6.0
116
- - ruby-2.6.5-rails-6.0
117
- - ruby-2.7.0-rails-5.2
118
- - ruby-2.6.5-rails-5.2
201
+ - ruby-3.0.0-rails-6.1
202
+ - ruby-2.7.2-rails-6.1
203
+ - ruby-3.0.0-rails-6.0
204
+ - ruby-2.7.2-rails-6.0
205
+ - ruby-2.7.2-rails-5.2
119
206
  filters:
120
207
  tags:
121
- only: /^[0-9]+\.[0-9]+\.[0-9]+(\.?RC[-\.]?\d*)?$/
208
+ only: /^[0-9]+\.[0-9]+\.[0-9]+(\.?(RC|rc)[-\.]?\w*)?$/
122
209
  branches:
123
210
  ignore: /.*/
124
- - ruby-2.7.0-rails-6.0:
211
+ - generate-and-push-docs:
212
+ context: org-global
213
+ requires:
214
+ - release
215
+ filters:
216
+ tags:
217
+ only: /^[0-9]+\.[0-9]+\.[0-9]+(\.?(RC|rc)[-\.]?\w*)?$/
218
+ branches:
219
+ ignore: /.*/
220
+ - ruby-3.0.0-rails-6.1:
125
221
  context: org-global
126
222
  filters:
127
223
  tags:
128
224
  only: &1 /.*/
129
- - ruby-2.6.5-rails-6.0:
225
+ - ruby-2.7.2-rails-6.1:
130
226
  context: org-global
131
227
  filters:
132
228
  tags:
133
229
  only: *1
134
- - ruby-2.7.0-rails-5.2:
230
+ - ruby-3.0.0-rails-6.0:
135
231
  context: org-global
136
232
  filters:
137
233
  tags:
138
234
  only: *1
139
- - ruby-2.6.5-rails-5.2:
235
+ - ruby-2.7.2-rails-6.0:
236
+ context: org-global
237
+ filters:
238
+ tags:
239
+ only: *1
240
+ - ruby-2.7.2-rails-5.2:
140
241
  context: org-global
141
242
  filters:
142
243
  tags:
@@ -150,11 +251,13 @@ workflows:
150
251
  only:
151
252
  - master
152
253
  jobs:
153
- - ruby-2.7.0-rails-6.0:
254
+ - ruby-3.0.0-rails-6.1:
255
+ context: org-global
256
+ - ruby-2.7.2-rails-6.1:
154
257
  context: org-global
155
- - ruby-2.6.5-rails-6.0:
258
+ - ruby-3.0.0-rails-6.0:
156
259
  context: org-global
157
- - ruby-2.7.0-rails-5.2:
260
+ - ruby-2.7.2-rails-6.0:
158
261
  context: org-global
159
- - ruby-2.6.5-rails-5.2:
262
+ - ruby-2.7.2-rails-5.2:
160
263
  context: org-global
data/.github/CODEOWNERS CHANGED
@@ -8,4 +8,4 @@
8
8
  # This file uses the GitHub CODEOWNERS convention to assign PR reviewers:
9
9
  # https://help.github.com/articles/about-codeowners/
10
10
 
11
- * @brettfishman @bwebster @stitchfix/devex
11
+ * @brettfishman @bwebster @stitchfix/runtime-infrastructure
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 2.7.0
1
+ ruby-2.7.2
data/Gemfile.rails-6.1 ADDED
@@ -0,0 +1,7 @@
1
+ # DO NOT MODIFY - this is managed by Git Reduce in goro
2
+ #
3
+ source 'https://stitchfix01.jfrog.io/stitchfix01/api/gems/eng-gems/'
4
+
5
+ gemspec
6
+
7
+ gem 'rails', '~> 6.1.0'
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
- * 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.
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. Instead of rendering web views, you render JSON (which is built into Rails).
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
 
@@ -35,14 +35,26 @@ Then, set it up:
35
35
 
36
36
  ### Upgrading from an older version
37
37
 
38
- * 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
39
- `api_clients` table:
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:
40
51
 
41
52
  ```
42
53
  > bin/rails generate stitches:add_enabled_to_api_clients
43
54
  > bin/rails generate stitches:add_deprecation
44
55
  ```
45
- * If you have a version lower than 3.6.0, you need to run one generator:
56
+
57
+ - If you have a version lower than 3.6.0, you need to run one generator:
46
58
 
47
59
  ```
48
60
  > bin/rails generate stitches:add_deprecation
@@ -59,8 +71,8 @@ class Api::V1::WidgetsController < ApiController
59
71
  if widget.valid?
60
72
  head 201
61
73
  else
62
- render json: {
63
- errors: Stitches::Errors.from_active_record_object(widget)
74
+ render json: {
75
+ errors: Stitches::Errors.from_active_record_object(widget)
64
76
  }, status: 422
65
77
  end
66
78
  end
@@ -73,44 +85,62 @@ private
73
85
  end
74
86
  ```
75
87
 
76
- If you think there's nothing special about this—you are correct. This is the vanillaest of vanilla Rails controllers, with a few
88
+ If you think there's nothing special about this—you are correct. This is the vanillaest of vanilla Rails controllers, with a few
77
89
  notable exceptions:
78
90
 
79
- * We aren't checking content type. A stitches-based microservice always uses JSON and refuses to route requests for non-JSON to
80
- you, so there's zero need to use `respond_to` and friends.
81
- * The error-building is structured and reliable.
82
- * This is an authenticated request. No request without proper authentication will be routed here, so you don't have to worry
83
- about it in your code.
84
- * This is a versioned request. While the URL will *not* contain `v1` in it, the `Accept` header will require a version and get
85
- routed here. If you make a V2, it's just a new controller and this concern is handled at the routing layer.
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.
86
98
 
87
- 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.
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.
88
100
 
89
101
  ## More Info
90
102
 
91
103
  See [the wiki](https://github.com/stitchfix/stitches/wiki/Setup) for how to setup stitches.
92
104
 
93
- * [Stitches Features](https://github.com/stitchfix/stitches/wiki/Features-of-Stitches) include:
105
+ - [Stitches Features](https://github.com/stitchfix/stitches/wiki/Features-of-Stitches) include:
94
106
  - Authorization via API key
95
107
  - Versioned requests via HTTP content types
96
108
  - Structured Errors
97
109
  - ISO 8601-formatted dates
98
110
  - Deprecation using the `Sunset` header
99
- * The [Generator](https://github.com/stitchfix/stitches/wiki/Generator) sets up some code in your app, so you can start writing
100
- APIs using vanilla Rails idioms:
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:
101
114
  - a "ping" controller that can validate your app is working
102
115
  - version routing based on content-type (requests for V2 use the same URL, but are serviced by a different controller)
103
116
  - An ApiClient Active Record
104
117
  - Acceptance tests that can produce API documentation as they test your app.
105
- * Stitches provides [testing support](https://github.com/stitchfix/stitches/wiki/Testing)
118
+ - Stitches provides [testing support](https://github.com/stitchfix/stitches/wiki/Testing)
119
+
120
+ ## API Key Caching
121
+
122
+ Since version 4.0.0, stitches now has the ability to cache API keys in
123
+ memory for a configurable amount of time. This may be an improvement for
124
+ some applications.
125
+
126
+ You must configure the API Cache for it be used.
127
+
128
+ ```ruby
129
+ Stitches.configure do |config|
130
+ config.max_cache_ttl = 5 # seconds
131
+ config.max_cache_size = 100 # how many keys to cache
132
+ end
133
+ ```
106
134
 
135
+ Your cache size should be
136
+ larger then the number of consumer keys your service has.
107
137
 
108
138
  ## Developing
109
139
 
110
- 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.
140
+ 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.
111
141
 
112
142
  Also, the integration test does a lot of "testing the implementation", but since Rails generators are notorious for silently
113
- failing with a successful result, we have to make sure that the various `inject_into_file` calls are actually working. Do not do
143
+ failing with a successful result, we have to make sure that the various `inject_into_file` calls are actually working. Do not do
114
144
  any fancy refactors here, just keep it up to date.
115
145
 
116
146
  ---
@@ -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
@@ -12,7 +12,6 @@ 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
- gem "stitches"
16
15
  gem "apitome"
17
16
  gem_group :development, :test do
18
17
  gem "rspec"
@@ -20,7 +19,9 @@ module Stitches
20
19
  gem "rspec_api_documentation"
21
20
  end
22
21
 
23
- run "bundle install"
22
+ Bundler.with_clean_env do
23
+ run "bundle install"
24
+ end
24
25
  generate "apitome:install"
25
26
  generate "rspec:install"
26
27
 
@@ -27,14 +27,7 @@ module Stitches
27
27
  if authorization
28
28
  if authorization =~ /#{@configuration.custom_http_auth_scheme}\s+key=(.*)\s*$/
29
29
  key = $1
30
-
31
- if ApiClient.column_names.include?("enabled")
32
- client = ApiClient.where(key: key, enabled: true).first
33
- else
34
- ActiveSupport::Deprecation.warn('api_keys is missing "enabled" column. Run "rails g stitches:add_enabled_to_api_clients"')
35
- client = ApiClient.where(key: key).first
36
- end
37
-
30
+ client = Stitches::ApiClientAccessWrapper.fetch_for_key(key)
38
31
  if client.present?
39
32
  env[@configuration.env_var_to_hold_api_client_primary_key] = client.id
40
33
  env[@configuration.env_var_to_hold_api_client] = client
@@ -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.
@@ -25,11 +27,6 @@ class Stitches::Configuration
25
27
  @allowlist_regexp = new_allowlist_regexp
26
28
  end
27
29
 
28
- def whitelist_regexp=(new_allowlist_regexp)
29
- self.allowlist_regexp = new_allowlist_regexp
30
- warn("⚠️ 'whitelist' is deprecated in stitches configuration, please use 'allowlist' or auto-update with:\n\n bin/rails g stitches:update_configuration\n\n⚠️ 'whitelist' will be removed in 4.0")
31
- end
32
-
33
30
  # The name of your custom http auth scheme. This must be set, and has no default
34
31
  def custom_http_auth_scheme
35
32
  @custom_http_auth_scheme.to_s
@@ -39,7 +36,7 @@ class Stitches::Configuration
39
36
  @custom_http_auth_scheme = NonNullString.new("custom_http_auth_scheme",new_custom_http_auth_scheme)
40
37
  end
41
38
 
42
- # The name of the environment variable that the ApiKey middleware should use to
39
+ # The name of the environment variable that the ApiKey middleware should use to
43
40
  # place the primary key of the authenticated ApiKey. For example, if a user provides
44
41
  # the api key 1234-1234-1234-1234, and that maps to the primary key 42 in your database,
45
42
  # the environment will contain "42" in the key provided here.
@@ -59,8 +56,40 @@ class Stitches::Configuration
59
56
  @env_var_to_hold_api_client= NonNullString.new("env_var_to_hold_api_client",new_env_var_to_hold_api_client)
60
57
  end
61
58
 
59
+ def max_cache_ttl
60
+ @max_cache_ttl.to_i
61
+ end
62
+
63
+ def max_cache_ttl=(new_max_cache_ttl)
64
+ @max_cache_ttl = NonNullInteger.new("max_cache_ttl", new_max_cache_ttl)
65
+ end
66
+
67
+ def max_cache_size
68
+ @max_cache_size.to_i
69
+ end
70
+
71
+ def max_cache_size=(new_max_cache_size)
72
+ @max_cache_size = NonNullInteger.new("max_cache_size", new_max_cache_size)
73
+ end
74
+
62
75
  private
63
76
 
77
+ class NonNullInteger
78
+ def initialize(name, value)
79
+ unless value.is_a?(Integer)
80
+ raise "#{name} must be an Integer, not a #{value.class}"
81
+ end
82
+
83
+ @value = value
84
+ end
85
+
86
+ def to_i
87
+ @value
88
+ end
89
+
90
+ alias to_integer to_i
91
+ end
92
+
64
93
  class NonNullString
65
94
  def initialize(name,string)
66
95
  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
@@ -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
@@ -1,9 +1,13 @@
1
1
  require 'active_support/time_with_zone'
2
2
 
3
3
  class ActiveSupport::TimeWithZone
4
- # We want dates to be a) in UTC and b) in ISO8601 always
4
+ # We want dates to always be in UTC
5
5
  def as_json(options = {})
6
- utc.iso8601
6
+ if utc?
7
+ super
8
+ else
9
+ utc.as_json(options)
10
+ end
7
11
  end
8
12
  end
9
13
 
@@ -1,19 +1,26 @@
1
1
  require_relative 'allowlist_middleware'
2
2
  module Stitches
3
- # A middleware that requires all API calls to be for versioned JSON. This means that the Accept
4
- # header (available to Rack apps as HTTP_ACCEPT) should be like so:
3
+ # A middleware that requires all API calls to be for versioned JSON or Protobuf.
4
+ #
5
+ # This means that the Accept header (available to Rack apps as HTTP_ACCEPT) should be like so:
5
6
  #
6
7
  # application/json; version=1
7
8
  #
8
9
  # This just checks that you've specified some numeric version. ApiVersionConstraint should be used
9
10
  # to "lock down" the versions you accept.
11
+ #
12
+ # Or in the case of a protobuf encoded payload the header should be like so:
13
+ #
14
+ # application/protobuf
15
+ #
16
+ # There isn't an accepted standard for protobuf encoded payloads but this form is common.
10
17
  class ValidMimeType < Stitches::AllowlistMiddleware
11
18
 
12
19
  protected
13
20
 
14
21
  def do_call(env)
15
22
  accept = String(env["HTTP_ACCEPT"])
16
- if accept =~ %r{application/json} && accept =~ %r{version=\d+}
23
+ if (accept =~ %r{application/json} && accept =~ %r{version=\d+}) || accept =~ %r{application/protobuf}
17
24
  @app.call(env)
18
25
  else
19
26
  not_acceptable_response(accept)
@@ -24,7 +31,7 @@ module Stitches
24
31
 
25
32
  def not_acceptable_response(accept_header)
26
33
  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"
34
+ body = "Not Acceptable - '#{accept_header}' didn't have the right mime type or version number. We only accept application/json with a version or application/protobuf"
28
35
  header = { "WWW-Authenticate" => accept_header }
29
36
  Rack::Response.new(body, status, header).finish
30
37
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Stitches
2
- VERSION = "3.8.2"
4
+ VERSION = '4.1.0RC2'
3
5
  end
@@ -14,7 +14,6 @@ require 'stitches/errors'
14
14
  require 'stitches/api_generator'
15
15
  require 'stitches/add_deprecation_generator'
16
16
  require 'stitches/add_enabled_to_api_clients_generator'
17
- require 'stitches/update_configuration_generator'
18
17
  require 'stitches/api_version_constraint'
19
18
  require 'stitches/api_key'
20
19
  require 'stitches/deprecation'
data/owners.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "owners": [
3
3
  {
4
- "team": "devex"
4
+ "team": "eng-runtime"
5
5
  }
6
6
  ]
7
7
  }
@@ -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(:api_clients) {
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,7 +25,8 @@ 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(:where).and_return(api_clients)
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") }
@@ -158,11 +157,11 @@ describe Stitches::ApiKey do
158
157
  end
159
158
 
160
159
  it "sets the api_client's ID in the environment" do
161
- expect(env[Stitches.configuration.env_var_to_hold_api_client_primary_key]).to eq(api_clients.first.id)
160
+ expect(env[Stitches.configuration.env_var_to_hold_api_client_primary_key]).to eq(api_client.id)
162
161
  end
163
162
 
164
163
  it "sets the api_client itself in the environment" do
165
- expect(env[Stitches.configuration.env_var_to_hold_api_client]).to eq(api_clients.first)
164
+ expect(env[Stitches.configuration.env_var_to_hold_api_client]).to eq(api_client)
166
165
  end
167
166
  end
168
167
 
@@ -177,7 +176,7 @@ describe Stitches::ApiKey do
177
176
  "HTTP_AUTHORIZATION" => "MyAwesomeInternalScheme key=foobar",
178
177
  }
179
178
  }
180
- let(:api_clients) { [] }
179
+ let(:api_client) { nil }
181
180
 
182
181
  it_behaves_like "an unauthorized response" do
183
182
  let(:expected_body) { "Unauthorized - key invalid" }
@@ -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,19 +116,34 @@ describe Stitches::Configuration do
102
116
  }.not_to raise_error
103
117
  end
104
118
  end
105
- context "deprecated options we want to support for backwards compatibility" do
106
119
 
107
- let(:logger) { double("logger") }
108
- before do
109
- allow(Rails).to receive(:logger).and_return(logger)
110
- allow(logger).to receive(:info)
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/)
111
126
  end
112
127
 
113
- it "'whitelist' still works for allowlist" do
114
- Stitches.configure do |config|
115
- config.whitelist_regexp = /foo/
116
- end
117
- expect(Stitches.configuration.allowlist_regexp).to eq(/foo/)
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/)
118
147
  end
119
148
  end
120
149
  end
@@ -109,35 +109,6 @@ RSpec.describe "Adding Stitches to a New Rails App", :integration do
109
109
  expect(include_line).to_not be_nil,lines.inspect
110
110
  end
111
111
 
112
- it "inserts can update old configuration" do
113
- run "bin/rails generate stitches:api"
114
-
115
- initializer = rails_root / "config" / "initializers" / "stitches.rb"
116
-
117
- initializer_contents = File.read(initializer).split(/\n/)
118
- found_initializer = false
119
- File.open(initializer,"w") do |file|
120
- initializer_contents.each do |line|
121
- if line =~ /allowlist/
122
- line = line.gsub("allowlist","whitelist")
123
- found_initializer = true
124
- end
125
- file.puts line
126
- end
127
- end
128
-
129
- raise "Didn't find 'allowlist' in the initializer?!" if !found_initializer
130
-
131
- run "bin/rails generate stitches:update_configuration"
132
-
133
- lines = File.read(initializer).split(/\n/)
134
- include_line = lines.detect { |line|
135
- line =~ /whitelist/
136
- }
137
-
138
- expect(include_line).to be_nil,lines.inspect
139
- end
140
-
141
112
  class RoutesFileAnalysis
142
113
  attr_reader :routes_file
143
114
  def initialize(routes_file, namespace: nil, module_scope: nil, resource: nil, mounted_engine: nil)
@@ -132,6 +132,22 @@ describe Stitches::ValidMimeType do
132
132
  end
133
133
  end
134
134
 
135
+ context "protbuf mime type" do
136
+ let(:env) {
137
+ {
138
+ "PATH_INFO" => "/api/ping",
139
+ "HTTP_ACCEPT" => "application/protobuf",
140
+ }
141
+ }
142
+
143
+ before do
144
+ @response = middleware.call(env)
145
+ end
146
+ it "calls through to the rest of the chain" do
147
+ expect(app).to have_received(:call).with(env)
148
+ end
149
+ end
150
+
135
151
  context "unacceptable responses" do
136
152
  before do
137
153
  @response = middleware.call(env)
data/stitches.gemspec CHANGED
@@ -20,6 +20,7 @@ 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("lru_redux")
23
24
 
24
25
  s.add_development_dependency("rspec", ">= 3")
25
26
  s.add_development_dependency("rake")
metadata CHANGED
@@ -1,17 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stitches
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.8.2
4
+ version: 4.1.0RC2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stitch Fix Engineering
8
8
  - Andrew Peterson
9
9
  - Dave Copeland
10
10
  - Jonathan Dean
11
- autorequire:
11
+ autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2020-01-17 00:00:00.000000000 Z
14
+ date: 2021-03-04 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: rails
@@ -41,6 +41,20 @@ dependencies:
41
41
  - - ">="
42
42
  - !ruby/object:Gem::Version
43
43
  version: '0'
44
+ - !ruby/object:Gem::Dependency
45
+ name: lru_redux
46
+ requirement: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: '0'
51
+ type: :runtime
52
+ prerelease: false
53
+ version_requirements: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
44
58
  - !ruby/object:Gem::Dependency
45
59
  name: rspec
46
60
  requirement: !ruby/object:Gem::Requirement
@@ -109,6 +123,7 @@ files:
109
123
  - Gemfile.rails-5.1
110
124
  - Gemfile.rails-5.2
111
125
  - Gemfile.rails-6.0
126
+ - Gemfile.rails-6.1
112
127
  - LICENSE.txt
113
128
  - README.md
114
129
  - Rakefile
@@ -117,6 +132,7 @@ files:
117
132
  - lib/stitches/add_deprecation_generator.rb
118
133
  - lib/stitches/add_enabled_to_api_clients_generator.rb
119
134
  - lib/stitches/allowlist_middleware.rb
135
+ - lib/stitches/api_client_access_wrapper.rb
120
136
  - lib/stitches/api_generator.rb
121
137
  - lib/stitches/api_key.rb
122
138
  - lib/stitches/api_version_constraint.rb
@@ -147,12 +163,11 @@ files:
147
163
  - lib/stitches/spec/have_api_error.rb
148
164
  - lib/stitches/spec/show_deprecation.rb
149
165
  - lib/stitches/spec/test_headers.rb
150
- - lib/stitches/update_configuration_generator.rb
151
166
  - lib/stitches/valid_mime_type.rb
152
167
  - lib/stitches/version.rb
153
- - lib/stitches/whitelisting_middleware.rb
154
168
  - lib/stitches_norailtie.rb
155
169
  - owners.json
170
+ - spec/api_client_access_wrapper_spec.rb
156
171
  - spec/api_key_spec.rb
157
172
  - spec/api_version_constraint_spec.rb
158
173
  - spec/configuration_spec.rb
@@ -170,7 +185,7 @@ homepage: https://github.com/stitchfix/stitches
170
185
  licenses:
171
186
  - MIT
172
187
  metadata: {}
173
- post_install_message:
188
+ post_install_message:
174
189
  rdoc_options: []
175
190
  require_paths:
176
191
  - lib
@@ -181,15 +196,16 @@ required_ruby_version: !ruby/object:Gem::Requirement
181
196
  version: '0'
182
197
  required_rubygems_version: !ruby/object:Gem::Requirement
183
198
  requirements:
184
- - - ">="
199
+ - - ">"
185
200
  - !ruby/object:Gem::Version
186
- version: '0'
201
+ version: 1.3.1
187
202
  requirements: []
188
- rubygems_version: 3.0.3
189
- signing_key:
203
+ rubygems_version: 3.1.4
204
+ signing_key:
190
205
  specification_version: 4
191
206
  summary: You'll be in stitches at how easy it is to create a service at Stitch Fix
192
207
  test_files:
208
+ - spec/api_client_access_wrapper_spec.rb
193
209
  - spec/api_key_spec.rb
194
210
  - spec/api_version_constraint_spec.rb
195
211
  - spec/configuration_spec.rb
@@ -1,16 +0,0 @@
1
- require 'rails/generators'
2
-
3
- module Stitches
4
- class UpdateConfigurationGenerator < Rails::Generators::Base
5
- include Rails::Generators::Migration
6
-
7
- source_root(File.expand_path(File.join(File.dirname(__FILE__),"generator_files")))
8
-
9
- desc "Change your configuration to use 'allowlist' so you'll be ready for 4.x"
10
- def update_to_allowlist
11
- gsub_file "config/initializers/stitches.rb", /whitelist/, "allowlist"
12
- puts "🎉 You are now good to go!"
13
- end
14
-
15
- end
16
- end
@@ -1,5 +0,0 @@
1
- require_relative "allowlist_middleware"
2
-
3
- module Stitches
4
- WhitelistingMiddleware = AllowlistMiddleware
5
- end