stitches 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.ruby-gemset +1 -0
  4. data/.ruby-version +1 -0
  5. data/Gemfile +3 -0
  6. data/Gemfile.lock +126 -0
  7. data/README.md +220 -0
  8. data/Rakefile +13 -0
  9. data/lib/stitches.rb +3 -0
  10. data/lib/stitches/api_generator.rb +97 -0
  11. data/lib/stitches/api_key.rb +59 -0
  12. data/lib/stitches/api_version_constraint.rb +32 -0
  13. data/lib/stitches/configuration.rb +77 -0
  14. data/lib/stitches/error.rb +9 -0
  15. data/lib/stitches/errors.rb +101 -0
  16. data/lib/stitches/generator_files/app/controllers/api.rb +2 -0
  17. data/lib/stitches/generator_files/app/controllers/api/api_controller.rb +19 -0
  18. data/lib/stitches/generator_files/app/controllers/api/v1.rb +2 -0
  19. data/lib/stitches/generator_files/app/controllers/api/v1/pings_controller.rb +20 -0
  20. data/lib/stitches/generator_files/app/controllers/api/v2.rb +2 -0
  21. data/lib/stitches/generator_files/app/controllers/api/v2/pings_controller.rb +20 -0
  22. data/lib/stitches/generator_files/app/models/api_client.rb +2 -0
  23. data/lib/stitches/generator_files/config/initializers/stitches.rb +14 -0
  24. data/lib/stitches/generator_files/db/migrate/create_api_clients.rb +11 -0
  25. data/lib/stitches/generator_files/db/migrate/enable_uuid_ossp_extension.rb +5 -0
  26. data/lib/stitches/generator_files/lib/tasks/generate_api_key.rake +10 -0
  27. data/lib/stitches/generator_files/spec/acceptance/ping_v1_spec.rb +46 -0
  28. data/lib/stitches/generator_files/spec/features/api_spec.rb +96 -0
  29. data/lib/stitches/railtie.rb +9 -0
  30. data/lib/stitches/render_timestamps_in_iso8601_in_json.rb +9 -0
  31. data/lib/stitches/spec.rb +4 -0
  32. data/lib/stitches/spec/api_clients.rb +5 -0
  33. data/lib/stitches/spec/be_iso_8601_utc_encoded.rb +10 -0
  34. data/lib/stitches/spec/have_api_error.rb +50 -0
  35. data/lib/stitches/spec/test_headers.rb +51 -0
  36. data/lib/stitches/valid_mime_type.rb +32 -0
  37. data/lib/stitches/version.rb +3 -0
  38. data/lib/stitches/whitelisting_middleware.rb +29 -0
  39. data/lib/stitches_norailtie.rb +17 -0
  40. data/spec/api_key_spec.rb +200 -0
  41. data/spec/api_version_constraint_spec.rb +33 -0
  42. data/spec/configuration_spec.rb +105 -0
  43. data/spec/errors_spec.rb +99 -0
  44. data/spec/spec/have_api_error_spec.rb +78 -0
  45. data/spec/spec_helper.rb +10 -0
  46. data/spec/valid_mime_type_spec.rb +166 -0
  47. data/stitches.gemspec +24 -0
  48. metadata +168 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 37541ea41d74a49dd809ec68dfcaf44a3bfb1221
4
+ data.tar.gz: d3d0eceb9b3a231d7535622f161c5100d6686c5d
5
+ SHA512:
6
+ metadata.gz: 6e566f80064dffdd94b5fd0f181d9751a1999869d778c970c3f791c3d673c062c4ae7da06acc60aa6aed0def394584ad962c6b7a9ff6b93f7cd9d6d046cd2172
7
+ data.tar.gz: b8f6160025a9cafe573452b9cc907c484f0909980a070709b2612b3d5f3645cafc93dcf7e973b4c797d47560b05a626cb16a6e62861770ccdcfdbcb04ba01d59
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ pkg
2
+ spec/reports
3
+ .vimrc
4
+ *.sw?
5
+ .idea/
6
+ config/database.yml
7
+ .tddium*
8
+ .DS_Store
9
+ .jhw-cache
10
+ **.orig
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ stitches
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.1.0
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://www.rubygems.org'
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,126 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ stitches (3.0.0)
5
+ rails
6
+ rspec (~> 3)
7
+
8
+ GEM
9
+ remote: https://www.rubygems.org/
10
+ specs:
11
+ actionmailer (4.2.0)
12
+ actionpack (= 4.2.0)
13
+ actionview (= 4.2.0)
14
+ activejob (= 4.2.0)
15
+ mail (~> 2.5, >= 2.5.4)
16
+ rails-dom-testing (~> 1.0, >= 1.0.5)
17
+ actionpack (4.2.0)
18
+ actionview (= 4.2.0)
19
+ activesupport (= 4.2.0)
20
+ rack (~> 1.6.0)
21
+ rack-test (~> 0.6.2)
22
+ rails-dom-testing (~> 1.0, >= 1.0.5)
23
+ rails-html-sanitizer (~> 1.0, >= 1.0.1)
24
+ actionview (4.2.0)
25
+ activesupport (= 4.2.0)
26
+ builder (~> 3.1)
27
+ erubis (~> 2.7.0)
28
+ rails-dom-testing (~> 1.0, >= 1.0.5)
29
+ rails-html-sanitizer (~> 1.0, >= 1.0.1)
30
+ activejob (4.2.0)
31
+ activesupport (= 4.2.0)
32
+ globalid (>= 0.3.0)
33
+ activemodel (4.2.0)
34
+ activesupport (= 4.2.0)
35
+ builder (~> 3.1)
36
+ activerecord (4.2.0)
37
+ activemodel (= 4.2.0)
38
+ activesupport (= 4.2.0)
39
+ arel (~> 6.0)
40
+ activesupport (4.2.0)
41
+ i18n (~> 0.7)
42
+ json (~> 1.7, >= 1.7.7)
43
+ minitest (~> 5.1)
44
+ thread_safe (~> 0.3, >= 0.3.4)
45
+ tzinfo (~> 1.1)
46
+ arel (6.0.0)
47
+ builder (3.2.2)
48
+ diff-lcs (1.2.5)
49
+ erubis (2.7.0)
50
+ globalid (0.3.0)
51
+ activesupport (>= 4.1.0)
52
+ hike (1.2.3)
53
+ i18n (0.7.0)
54
+ json (1.8.2)
55
+ loofah (2.0.1)
56
+ nokogiri (>= 1.5.9)
57
+ mail (2.6.3)
58
+ mime-types (>= 1.16, < 3)
59
+ mime-types (2.4.3)
60
+ mini_portile (0.6.2)
61
+ minitest (5.5.1)
62
+ multi_json (1.10.1)
63
+ nokogiri (1.6.6.2)
64
+ mini_portile (~> 0.6.0)
65
+ rack (1.6.0)
66
+ rack-test (0.6.3)
67
+ rack (>= 1.0)
68
+ rails (4.2.0)
69
+ actionmailer (= 4.2.0)
70
+ actionpack (= 4.2.0)
71
+ actionview (= 4.2.0)
72
+ activejob (= 4.2.0)
73
+ activemodel (= 4.2.0)
74
+ activerecord (= 4.2.0)
75
+ activesupport (= 4.2.0)
76
+ bundler (>= 1.3.0, < 2.0)
77
+ railties (= 4.2.0)
78
+ sprockets-rails
79
+ rails-deprecated_sanitizer (1.0.3)
80
+ activesupport (>= 4.2.0.alpha)
81
+ rails-dom-testing (1.0.5)
82
+ activesupport (>= 4.2.0.beta, < 5.0)
83
+ nokogiri (~> 1.6.0)
84
+ rails-deprecated_sanitizer (>= 1.0.1)
85
+ rails-html-sanitizer (1.0.1)
86
+ loofah (~> 2.0)
87
+ railties (4.2.0)
88
+ actionpack (= 4.2.0)
89
+ activesupport (= 4.2.0)
90
+ rake (>= 0.8.7)
91
+ thor (>= 0.18.1, < 2.0)
92
+ rake (10.3.2)
93
+ rspec (3.2.0)
94
+ rspec-core (~> 3.2.0)
95
+ rspec-expectations (~> 3.2.0)
96
+ rspec-mocks (~> 3.2.0)
97
+ rspec-core (3.2.0)
98
+ rspec-support (~> 3.2.0)
99
+ rspec-expectations (3.2.0)
100
+ diff-lcs (>= 1.2.0, < 2.0)
101
+ rspec-support (~> 3.2.0)
102
+ rspec-mocks (3.2.0)
103
+ diff-lcs (>= 1.2.0, < 2.0)
104
+ rspec-support (~> 3.2.0)
105
+ rspec-support (3.2.0)
106
+ sprockets (2.12.3)
107
+ hike (~> 1.2)
108
+ multi_json (~> 1.0)
109
+ rack (~> 1.0)
110
+ tilt (~> 1.1, != 1.3.0)
111
+ sprockets-rails (2.2.4)
112
+ actionpack (>= 3.0)
113
+ activesupport (>= 3.0)
114
+ sprockets (>= 2.8, < 4.0)
115
+ thor (0.19.1)
116
+ thread_safe (0.3.4)
117
+ tilt (1.4.1)
118
+ tzinfo (1.2.2)
119
+ thread_safe (~> 0.1)
120
+
121
+ PLATFORMS
122
+ ruby
123
+
124
+ DEPENDENCIES
125
+ rake
126
+ stitches!
data/README.md ADDED
@@ -0,0 +1,220 @@
1
+ You'll be in stitches at how easy it is to get your API service up and running!
2
+
3
+ This gem provides:
4
+
5
+ * Rails plugin to handle api key and versioned requests
6
+ * Generator to get you set up and validate your API before you write any code
7
+ * Spec helpers to use when building your API
8
+
9
+ ## To install
10
+
11
+ Add to your `Gemfile`:
12
+
13
+ ```ruby
14
+ gem 'stitches'
15
+ ```
16
+
17
+ Then of course:
18
+
19
+ ```
20
+ bundle install
21
+ ```
22
+
23
+ If your app is brand new or you don't already have rspec or Apitome, be sure to run those generators and set them up first. You can skip this if you've done either of these before:
24
+
25
+ ```
26
+ rails generate rspec:install
27
+ rails generate apitome:install
28
+ ```
29
+
30
+ Then, run the Stitches generator:
31
+
32
+ ```
33
+ rails g stitches:api
34
+ ```
35
+
36
+ Run database migrations (do `rake db:create` first if you haven't yet set up a database)
37
+
38
+ ```
39
+ rake db:migrate
40
+ ```
41
+
42
+
43
+ ## Test the install
44
+
45
+ In your app run:
46
+
47
+ ```
48
+ rake generate_api_key[some_name_you_pick]
49
+ ```
50
+
51
+ Then follow the instructions it outputs.
52
+
53
+ If you are doing this locally and get an error like the following:
54
+
55
+ > WEBrick::HTTPStatus::LengthRequired
56
+
57
+ that's because WEBrick doesn't want an empty body. Throw on a `-d ''` to the end of the command.
58
+
59
+ ## Rails Plugin
60
+
61
+ Just using this gem will get you:
62
+
63
+ * Middleware that requires an api key in the request. See `Stitches::ApiKey`.
64
+ * Middleware that requires a versioned JSON mime type. We aren't using version numbers in the URLs but putting versions in the `Accept` header. See `Stitches::ValidMimeType`.
65
+ * Monkeypatch ActiveSupport's `TimeWithZone` to use ISO8601 UTC as the encoding for all timestamps. This ensures that every JSON response is using the same timezone and can be parsed properly with JavaScript.
66
+ * `Error` and `Errors` objects for creating error reponses that will be rendered properly in API responses. These can work with exceptions, ActiveRecord objects, or whatever you like.
67
+
68
+ The plugin works in conjunction with other pieces of this gem set up by using the generator.
69
+
70
+ ### A word on errors
71
+
72
+ The error format generated by `Errors` and friends is designed for two purposes:
73
+
74
+ * provide a "programmer key" upon which logic can be based for each error
75
+ * provide a message to explain the error to the programmer and, in times of desperation, the user
76
+
77
+ The idea is that the caller should not have made a call that generates an error, although this isn't always 100% possible to avoid. As such, the error messages that come back are not as "rich" as you might get
78
+ from ActiveRecord. APIs aren't intended to be used for validating user input. The messages are aimed at a programmer trying to understand why a call failed.
79
+
80
+ ## Generator
81
+
82
+ When you run the generator via `rails g stitches:api`, you'll get a lot of handy setup done for you:
83
+
84
+ * Two "ping" controllers that do nothing but respond to requests. This are useful when creating an API client or when validating
85
+ a deployed instance, as they will succeed when all the moving parts are working, but won't actually hit a database or depend on
86
+ any particular business logic.
87
+ * Routes configured using `Stitches::ApiVersionConstraint` to route to your ping controllers based on the version in
88
+ the `Accept` header. This allows your api client to fully validate that it's using the versioning system properly.
89
+ * An `ApiClient` model and migration for your database. This additionally sets up UUID support in Postgres.
90
+ * An `api_spec.rb` spec that will spin up your application and check that all the RESTful/HTTP stuff is working as designed.
91
+ * A means to write API documentation via [rspec_api_documentation](https://github.com/zipmark/rspec_api_documentation), and serve it nicely via [apitome](https://github.com/modeset/apitome). You write tests using a slightly modified DSL that allows you to document the API. The produced documentation shows the headers and wire formats with your test data.
92
+
93
+ Once this is done, you can (and possibly should) deploy your app to validate everything's working before you get too into your
94
+ business logic.
95
+
96
+ Once you start writing your app, there's a few spec helpers available.
97
+
98
+ ## Generator-provided tests
99
+
100
+ The generator will provide some tests for your app to test a base level of functionality. To use them, add the Capybara gem to your app:
101
+
102
+ ```
103
+ gem 'capybara'
104
+ ```
105
+
106
+ And in your `spec_helper.rb`:
107
+
108
+ ```ruby
109
+ require 'capybara/rails'
110
+ ```
111
+
112
+ ## Spec Helpers
113
+
114
+ ```ruby
115
+ # in your spec_helper.rb
116
+ require 'stitches/spec'
117
+ ```
118
+
119
+ ### `have_api_error`
120
+
121
+ This can be used in a feature spec on a response object to check that the error format is in our "canonical" format, which is as
122
+ follows:
123
+
124
+ ```json
125
+ {
126
+ "errors": [
127
+ {
128
+ "code": "some_code",
129
+ "message": "some readable message"
130
+ },
131
+ {
132
+ "code": "some_other_code",
133
+ "message": "some other readable message"
134
+ }
135
+ # etc.
136
+ ]
137
+ }
138
+ ```
139
+
140
+ To assert this in your specs:
141
+
142
+ ```ruby
143
+ expect(response).to have_api_error(status: 404,
144
+ code: "not_found",
145
+ message: "the exact error message")
146
+
147
+ # regexps also work
148
+ expect(response).to have_api_error(status: 404,
149
+ code: "name_invalid",
150
+ message: /can't be blank/)
151
+ ```
152
+
153
+ Omitting `status` will use a default of 422, which is Rails' default for validation errors.
154
+
155
+ ### `be_iso8601_utc_encoded`
156
+
157
+ This can be used to ensure that your JSON-encoded timestamps meet the recommended format of IS8601 UTC-encoded strings.
158
+
159
+ ```ruby
160
+ expect(json["created_at"]).to be_iso8601_utc_encoded
161
+ ```
162
+
163
+ ### `TestHeaders`
164
+
165
+ This class is useful when creating headers in feature specs that will conform to what's needed by the API you are building.
166
+ The generated `api_spec.rb` is a complete example, but here's a sampling:
167
+
168
+ ```ruby
169
+ # Create headers for a version 2 request
170
+ headers = TestHeaders.new(version: 2)
171
+ post "/api/ping", {}.to_json, headers.headers
172
+
173
+ # Create headers that have the wrong mime type in them
174
+ headers = TestHeaders.new(mime_type: "application/foobar")
175
+ post "/api/ping", {}.to_json, headers.headers
176
+ ```
177
+
178
+ ### `ApiClients` module
179
+
180
+ Mixing this in provides the method `api_client` that will return the one api client. This is because fixtures have been so flaky
181
+ in our tests, and factories don't always work for stuff like this. Also, why require the use of factories?
182
+
183
+ ## Configuration
184
+
185
+ You'll see in the initializer that you can configure aspects of the library globally. `Stitches::Configuration` documents the available
186
+ options.
187
+
188
+ ## Avoiding auto-magical Set-up
189
+
190
+ Instead of requiring `stitches`, you can avoid the railtie and configure things yourself:
191
+
192
+ ```
193
+ # Gemfile
194
+ gem 'stitches', require: false
195
+
196
+ # config/initializers/stitches.rb
197
+ require 'stitches_norailtie'
198
+
199
+ Stitches.configure do |config|
200
+ config.whatever = :foo
201
+ end
202
+
203
+ # config/application.rb
204
+ config.app_middleware.use "Stitches::ApiKey", except: %r{/super-secret}
205
+ config.app_middleware.use "Stitches::ValidMimeType", except: %r{/super-secret}
206
+ # or whatever you want to do
207
+ ```
208
+
209
+ ## Developing
210
+
211
+ Although `Stitches.configuration` is global, do not depend directly on that in your logic. Instead, allow all classes to receive a
212
+ configuration object in their constructor. This makes the classes easier to deal with and change, without incurring much of a real cost to development.
213
+ Glaobl symbols suck, but are convienient. This is how you make the most of it.
214
+
215
+
216
+ ---
217
+
218
+ Provided with love by your friends at [Stitch Fix Engineering](http://technology.stitchfix.com)
219
+
220
+ ![stitches](https://s3.amazonaws.com/stitchfix-stitches/stitches.png)
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rubygems/package_task'
3
+ require 'rspec/core/rake_task'
4
+
5
+ $: << File.join(File.dirname(__FILE__),'lib')
6
+
7
+ include Rake::DSL
8
+
9
+ gemspec = eval(File.read('stitches.gemspec'))
10
+ Gem::PackageTask.new(gemspec) {}
11
+ RSpec::Core::RakeTask.new(:spec)
12
+
13
+ task :default => :spec
data/lib/stitches.rb ADDED
@@ -0,0 +1,3 @@
1
+ require 'stitches_norailtie'
2
+ require 'stitches/railtie'
3
+ require 'apitome'
@@ -0,0 +1,97 @@
1
+ require 'rails/generators'
2
+
3
+ module Stitches
4
+ class ApiGenerator < Rails::Generators::Base
5
+ include Rails::Generators::Migration
6
+
7
+ source_root(File.expand_path(File.join(File.dirname(__FILE__),"generator_files")))
8
+
9
+ def self.next_migration_number(path)
10
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
11
+ end
12
+
13
+ desc "Bootstraps your API service with a basic ping controller and spec to ensure everything is setup properly"
14
+ def bootstrap_api
15
+ inject_into_file "Gemfile", after: /^gem ["']pg['"].*$/ do<<-GEM
16
+
17
+ gem "apitome"
18
+ gem "responders"
19
+ GEM
20
+ end
21
+ inject_into_file "config/routes.rb", before: /^end/ do<<-ROUTES
22
+ namespace :api do
23
+ scope module: :v1, constraints: Stitches::ApiVersionConstraint.new(1) do
24
+ resource 'ping', only: [ :create ]
25
+ # Add your V1 resources here
26
+ end
27
+ scope module: :v2, constraints: Stitches::ApiVersionConstraint.new(2) do
28
+ resource 'ping', only: [ :create ]
29
+ # This is here simply to validate that versioning is working
30
+ # as well as for your client to be able to validate this as well.
31
+ end
32
+ end
33
+ api_docs = Rack::Auth::Basic.new(Apitome::Engine ) do |_,password|
34
+ password == ENV['HTTP_AUTH_PASSWORD']
35
+ end
36
+ mount api_docs, at: "docs"
37
+ ROUTES
38
+ end
39
+
40
+ run 'bundle install'
41
+
42
+ copy_file "app/controllers/api.rb"
43
+ copy_file "app/controllers/api/api_controller.rb"
44
+ copy_file "app/controllers/api/v1.rb"
45
+ copy_file "app/controllers/api/v2.rb"
46
+ copy_file "app/controllers/api/v1/pings_controller.rb"
47
+ copy_file "app/controllers/api/v2/pings_controller.rb"
48
+ copy_file "app/models/api_client.rb"
49
+ copy_file "config/initializers/stitches.rb"
50
+ copy_file "lib/tasks/generate_api_key.rake"
51
+ copy_file "spec/features/api_spec.rb", "spec/features/api_spec.rb"
52
+ copy_file "spec/acceptance/ping_v1_spec.rb", "spec/acceptance/ping_v1_spec.rb"
53
+
54
+ inject_into_file "Gemfile", after: /^group :test, :development do.*$/ do<<-GEM
55
+
56
+ gem "rspec_api_documentation"
57
+ GEM
58
+ end
59
+ run 'bundle install'
60
+
61
+ migration_template "db/migrate/enable_uuid_ossp_extension.rb", "db/migrate/enable_uuid_ossp_extension.rb"
62
+ sleep 1 # allow clock to tick so we get different numbers
63
+ migration_template "db/migrate/create_api_clients.rb", "db/migrate/create_api_clients.rb"
64
+
65
+ inject_into_file 'spec/spec_helper.rb', %q{
66
+ config.include RSpec::Rails::RequestExampleGroup, type: :feature
67
+ }, before: /^end/
68
+
69
+ inject_into_file 'spec/spec_helper.rb', before: /^RSpec.configure/ do<<-REQUIRE
70
+ require 'stitches/spec'
71
+ REQUIRE
72
+ end
73
+
74
+ append_to_file 'spec/spec_helper.rb' do<<-RSPEC_API
75
+ RspecApiDocumentation.configure do |config|
76
+ config.format = :json
77
+ config.request_headers_to_include = %w(
78
+ Accept
79
+ Content-Type
80
+ Authorization
81
+ If-Modified-Since
82
+ )
83
+ config.response_headers_to_include = %w(
84
+ Last-Modified
85
+ ETag
86
+ )
87
+ config.api_name = "YOUR SERVICE NAME HERE"
88
+ end
89
+ RSPEC_API
90
+ end
91
+ run "rails g apitome:install"
92
+ gsub_file 'config/initializers/apitome.rb', /config.mount_at = .*$/, "config.mount_at = nil"
93
+ gsub_file 'config/initializers/apitome.rb', /config.title = .*$/, "config.title = 'Service Documentation'"
94
+
95
+ end
96
+ end
97
+ end