stitches 3.8.2 → 4.1.0RC2

Sign up to get free protection for your applications and to get access to all the features.
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