sober_swag 0.1.0 → 0.2.0

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.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/.github/config/rubocop_linter_action.yml +5 -0
  3. data/.github/workflows/lint.yml +15 -0
  4. data/.github/workflows/ruby.yml +23 -1
  5. data/.gitignore +3 -0
  6. data/.rubocop.yml +73 -1
  7. data/.ruby-version +1 -1
  8. data/Gemfile.lock +29 -5
  9. data/README.md +109 -0
  10. data/bin/console +15 -14
  11. data/docs/serializers.md +203 -0
  12. data/example/.rspec +1 -0
  13. data/example/.ruby-version +1 -1
  14. data/example/Gemfile +10 -6
  15. data/example/Gemfile.lock +96 -76
  16. data/example/app/controllers/people_controller.rb +37 -21
  17. data/example/app/controllers/posts_controller.rb +102 -0
  18. data/example/app/models/application_record.rb +3 -0
  19. data/example/app/models/person.rb +6 -0
  20. data/example/app/models/post.rb +9 -0
  21. data/example/app/output_objects/person_errors_output_object.rb +5 -0
  22. data/example/app/output_objects/person_output_object.rb +15 -0
  23. data/example/app/output_objects/post_output_object.rb +10 -0
  24. data/example/bin/bundle +24 -20
  25. data/example/bin/rails +1 -1
  26. data/example/bin/rake +1 -1
  27. data/example/config/application.rb +11 -7
  28. data/example/config/environments/development.rb +0 -1
  29. data/example/config/environments/production.rb +3 -3
  30. data/example/config/puma.rb +5 -5
  31. data/example/config/routes.rb +3 -0
  32. data/example/config/spring.rb +4 -4
  33. data/example/db/migrate/20200311152021_create_people.rb +0 -1
  34. data/example/db/migrate/20200603172347_create_posts.rb +11 -0
  35. data/example/db/schema.rb +16 -7
  36. data/example/spec/rails_helper.rb +64 -0
  37. data/example/spec/requests/people/create_spec.rb +52 -0
  38. data/example/spec/requests/people/get_spec.rb +35 -0
  39. data/example/spec/requests/people/index_spec.rb +69 -0
  40. data/example/spec/spec_helper.rb +94 -0
  41. data/lib/sober_swag.rb +6 -3
  42. data/lib/sober_swag/compiler/error.rb +2 -0
  43. data/lib/sober_swag/compiler/path.rb +2 -5
  44. data/lib/sober_swag/compiler/paths.rb +0 -1
  45. data/lib/sober_swag/compiler/type.rb +28 -15
  46. data/lib/sober_swag/controller.rb +16 -11
  47. data/lib/sober_swag/controller/route.rb +18 -21
  48. data/lib/sober_swag/controller/undefined_body_error.rb +3 -0
  49. data/lib/sober_swag/controller/undefined_path_error.rb +3 -0
  50. data/lib/sober_swag/controller/undefined_query_error.rb +3 -0
  51. data/lib/sober_swag/input_object.rb +28 -0
  52. data/lib/sober_swag/nodes/array.rb +1 -1
  53. data/lib/sober_swag/nodes/base.rb +2 -4
  54. data/lib/sober_swag/nodes/binary.rb +2 -1
  55. data/lib/sober_swag/nodes/enum.rb +4 -2
  56. data/lib/sober_swag/nodes/list.rb +0 -1
  57. data/lib/sober_swag/nodes/primitive.rb +6 -5
  58. data/lib/sober_swag/output_object.rb +102 -0
  59. data/lib/sober_swag/output_object/definition.rb +30 -0
  60. data/lib/sober_swag/{blueprint → output_object}/field.rb +14 -4
  61. data/lib/sober_swag/{blueprint → output_object}/field_syntax.rb +1 -1
  62. data/lib/sober_swag/{blueprint → output_object}/view.rb +15 -6
  63. data/lib/sober_swag/parser.rb +5 -3
  64. data/lib/sober_swag/serializer.rb +5 -2
  65. data/lib/sober_swag/serializer/array.rb +12 -0
  66. data/lib/sober_swag/serializer/base.rb +50 -1
  67. data/lib/sober_swag/serializer/conditional.rb +15 -2
  68. data/lib/sober_swag/serializer/field_list.rb +29 -6
  69. data/lib/sober_swag/serializer/mapped.rb +12 -2
  70. data/lib/sober_swag/serializer/meta.rb +35 -0
  71. data/lib/sober_swag/serializer/optional.rb +17 -2
  72. data/lib/sober_swag/serializer/primitive.rb +4 -1
  73. data/lib/sober_swag/server.rb +83 -0
  74. data/lib/sober_swag/types.rb +3 -0
  75. data/lib/sober_swag/version.rb +1 -1
  76. data/sober_swag.gemspec +6 -4
  77. metadata +77 -44
  78. data/example/person.json +0 -4
  79. data/example/test/controllers/.keep +0 -0
  80. data/example/test/fixtures/.keep +0 -0
  81. data/example/test/fixtures/files/.keep +0 -0
  82. data/example/test/fixtures/people.yml +0 -11
  83. data/example/test/integration/.keep +0 -0
  84. data/example/test/models/.keep +0 -0
  85. data/example/test/models/person_test.rb +0 -7
  86. data/example/test/test_helper.rb +0 -13
  87. data/lib/sober_swag/blueprint.rb +0 -113
  88. data/lib/sober_swag/path.rb +0 -8
  89. data/lib/sober_swag/path/integer.rb +0 -21
  90. data/lib/sober_swag/path/lit.rb +0 -41
  91. data/lib/sober_swag/path/literal.rb +0 -29
  92. data/lib/sober_swag/path/param.rb +0 -33
@@ -1,3 +1,6 @@
1
+ ##
2
+ # Base class for all other models.
3
+ # Does nothing interesting.
1
4
  class ApplicationRecord < ActiveRecord::Base
2
5
  self.abstract_class = true
3
6
  end
@@ -1,2 +1,8 @@
1
+ ##
2
+ # Extremely basic person example record.
1
3
  class Person < ApplicationRecord
4
+ has_many :posts, dependent: :destroy
5
+
6
+ validates :first_name, presence: true
7
+ validates :last_name, presence: true
2
8
  end
@@ -0,0 +1,9 @@
1
+ ##
2
+ # Extremely basic post example record.
3
+ # This is a blog post! Wow.
4
+ class Post < ApplicationRecord
5
+ belongs_to :person
6
+
7
+ validates :title, presence: true
8
+ validates :body, presence: true
9
+ end
@@ -0,0 +1,5 @@
1
+ PersonErrorsOutputObject = SoberSwag::OutputObject.define do
2
+ identifier 'PersonErrors'
3
+ field :first_name, primitive(:String).array.optional
4
+ field :last_name, primitive(:String).array.optional
5
+ end
@@ -0,0 +1,15 @@
1
+ PersonOutputObject = SoberSwag::OutputObject.define do
2
+ identifier 'Person'
3
+ field :id, primitive(:Integer).meta(description: 'Unique ID')
4
+ field :first_name, primitive(:String).meta(description: <<~MARKDOWN)
5
+ This is the first name of a person.
6
+ Note that you can't use this as a unique identifier, and you really should understand how names work before using this.
7
+ [Falsehoods programmers believe about names](https://www.kalzumeus.com/2010/06/17/falsehoods-programmers-believe-about-names/)
8
+ is a good thing to read!
9
+ MARKDOWN
10
+ field :last_name, primitive(:String)
11
+
12
+ view :detail do
13
+ field :posts, -> { PostOutputObject.array }
14
+ end
15
+ end
@@ -0,0 +1,10 @@
1
+ PostOutputObject = SoberSwag::OutputObject.define do
2
+ identifier 'Post'
3
+ field :id, primitive(:Integer)
4
+ field :title, primitive(:String)
5
+ field :body, primitive(:String)
6
+
7
+ view :detail do
8
+ field :person, -> { PersonOutputObject.view(:base) }
9
+ end
10
+ end
@@ -8,7 +8,7 @@
8
8
  # this file is here to facilitate running it.
9
9
  #
10
10
 
11
- require "rubygems"
11
+ require 'rubygems'
12
12
 
13
13
  m = Module.new do
14
14
  module_function
@@ -18,36 +18,36 @@ m = Module.new do
18
18
  end
19
19
 
20
20
  def env_var_version
21
- ENV["BUNDLER_VERSION"]
21
+ ENV['BUNDLER_VERSION']
22
22
  end
23
23
 
24
24
  def cli_arg_version
25
25
  return unless invoked_as_script? # don't want to hijack other binstubs
26
- return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update`
26
+ return unless 'update'.start_with?(ARGV.first || ' ') # must be running `bundle update`
27
+
27
28
  bundler_version = nil
28
29
  update_index = nil
29
30
  ARGV.each_with_index do |a, i|
30
- if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN
31
- bundler_version = a
32
- end
31
+ bundler_version = a if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN
33
32
  next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/
34
- bundler_version = $1
33
+
34
+ bundler_version = Regexp.last_match(1)
35
35
  update_index = i
36
36
  end
37
37
  bundler_version
38
38
  end
39
39
 
40
40
  def gemfile
41
- gemfile = ENV["BUNDLE_GEMFILE"]
41
+ gemfile = ENV['BUNDLE_GEMFILE']
42
42
  return gemfile if gemfile && !gemfile.empty?
43
43
 
44
- File.expand_path("../../Gemfile", __FILE__)
44
+ File.expand_path('../Gemfile', __dir__)
45
45
  end
46
46
 
47
47
  def lockfile
48
48
  lockfile =
49
49
  case File.basename(gemfile)
50
- when "gems.rb" then gemfile.sub(/\.rb$/, gemfile)
50
+ when 'gems.rb' then gemfile.sub(/\.rb$/, gemfile)
51
51
  else "#{gemfile}.lock"
52
52
  end
53
53
  File.expand_path(lockfile)
@@ -55,15 +55,17 @@ m = Module.new do
55
55
 
56
56
  def lockfile_version
57
57
  return unless File.file?(lockfile)
58
+
58
59
  lockfile_contents = File.read(lockfile)
59
60
  return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/
61
+
60
62
  Regexp.last_match(1)
61
63
  end
62
64
 
63
65
  def bundler_version
64
66
  @bundler_version ||=
65
67
  env_var_version || cli_arg_version ||
66
- lockfile_version
68
+ lockfile_version
67
69
  end
68
70
 
69
71
  def bundler_requirement
@@ -73,28 +75,32 @@ m = Module.new do
73
75
 
74
76
  requirement = bundler_gem_version.approximate_recommendation
75
77
 
76
- return requirement unless Gem::Version.new(Gem::VERSION) < Gem::Version.new("2.7.0")
78
+ return requirement unless Gem::Version.new(Gem::VERSION) < Gem::Version.new('2.7.0')
77
79
 
78
- requirement += ".a" if bundler_gem_version.prerelease?
80
+ requirement += '.a' if bundler_gem_version.prerelease?
79
81
 
80
82
  requirement
81
83
  end
82
84
 
83
85
  def load_bundler!
84
- ENV["BUNDLE_GEMFILE"] ||= gemfile
86
+ ENV['BUNDLE_GEMFILE'] ||= gemfile
85
87
 
86
88
  activate_bundler
87
89
  end
88
90
 
89
91
  def activate_bundler
90
92
  gem_error = activation_error_handling do
91
- gem "bundler", bundler_requirement
93
+ gem 'bundler', bundler_requirement
92
94
  end
93
95
  return if gem_error.nil?
96
+
94
97
  require_error = activation_error_handling do
95
- require "bundler/version"
98
+ require 'bundler/version'
99
+ end
100
+ if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION))
101
+ return
96
102
  end
97
- return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION))
103
+
98
104
  warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`"
99
105
  exit 42
100
106
  end
@@ -109,6 +115,4 @@ end
109
115
 
110
116
  m.load_bundler!
111
117
 
112
- if m.invoked_as_script?
113
- load Gem.bin_path("bundler", "bundle")
114
- end
118
+ load Gem.bin_path('bundler', 'bundle') if m.invoked_as_script?
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
  begin
3
- load File.expand_path('../spring', __FILE__)
3
+ load File.expand_path('spring', __dir__)
4
4
  rescue LoadError => e
5
5
  raise unless e.message.include?('spring')
6
6
  end
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
  begin
3
- load File.expand_path('../spring', __FILE__)
3
+ load File.expand_path('spring', __dir__)
4
4
  rescue LoadError => e
5
5
  raise unless e.message.include?('spring')
6
6
  end
@@ -1,29 +1,33 @@
1
1
  require_relative 'boot'
2
2
 
3
- require "rails"
3
+ require 'rails'
4
4
  # Pick the frameworks you want:
5
- require "active_model/railtie"
6
- require "active_job/railtie"
7
- require "active_record/railtie"
5
+ require 'active_model/railtie'
6
+ require 'active_job/railtie'
7
+ require 'active_record/railtie'
8
8
  # require "active_storage/engine"
9
- require "action_controller/railtie"
9
+ require 'action_controller/railtie'
10
10
  # require "action_mailer/railtie"
11
11
  # require "action_mailbox/engine"
12
12
  # require "action_text/engine"
13
- require "action_view/railtie"
13
+ require 'action_view/railtie'
14
14
  # require "action_cable/engine"
15
15
  # require "sprockets/railtie"
16
- require "rails/test_unit/railtie"
16
+ require 'rails/test_unit/railtie'
17
17
 
18
18
  # Require the gems listed in Gemfile, including any gems
19
19
  # you've limited to :test, :development, or :production.
20
20
  Bundler.require(*Rails.groups)
21
21
 
22
22
  module Example
23
+ ##
24
+ # Toplevel rails app.
23
25
  class Application < Rails::Application
24
26
  # Initialize configuration defaults for originally generated Rails version.
25
27
  config.load_defaults 6.0
26
28
 
29
+ config.autoload_paths << 'app/blueprints'
30
+
27
31
  # Settings in config/environments/* take precedence over those specified here.
28
32
  # Application configuration can go into files in config/initializers
29
33
  # -- all .rb files in that directory are automatically loaded after loading
@@ -34,7 +34,6 @@ Rails.application.configure do
34
34
  # Highlight code that triggered database queries in logs.
35
35
  config.active_record.verbose_query_logs = true
36
36
 
37
-
38
37
  # Raises error for missing translations.
39
38
  # config.action_view.raise_on_missing_translations = true
40
39
 
@@ -11,7 +11,7 @@ Rails.application.configure do
11
11
  config.eager_load = true
12
12
 
13
13
  # Full error reports are disabled and caching is turned on.
14
- config.consider_all_requests_local = false
14
+ config.consider_all_requests_local = false
15
15
 
16
16
  # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"]
17
17
  # or in config/master.key. This key is used to decrypt credentials (and other encrypted files).
@@ -36,7 +36,7 @@ Rails.application.configure do
36
36
  config.log_level = :debug
37
37
 
38
38
  # Prepend all log lines with the following tags.
39
- config.log_tags = [ :request_id ]
39
+ config.log_tags = [:request_id]
40
40
 
41
41
  # Use a different cache store in production.
42
42
  # config.cache_store = :mem_cache_store
@@ -59,7 +59,7 @@ Rails.application.configure do
59
59
  # require 'syslog/logger'
60
60
  # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
61
61
 
62
- if ENV["RAILS_LOG_TO_STDOUT"].present?
62
+ if ENV['RAILS_LOG_TO_STDOUT'].present?
63
63
  logger = ActiveSupport::Logger.new(STDOUT)
64
64
  logger.formatter = config.log_formatter
65
65
  config.logger = ActiveSupport::TaggedLogging.new(logger)
@@ -4,20 +4,20 @@
4
4
  # the maximum value specified for Puma. Default is set to 5 threads for minimum
5
5
  # and maximum; this matches the default thread size of Active Record.
6
6
  #
7
- max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
8
- min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
7
+ max_threads_count = ENV.fetch('RAILS_MAX_THREADS', 5)
8
+ min_threads_count = ENV.fetch('RAILS_MIN_THREADS', max_threads_count)
9
9
  threads min_threads_count, max_threads_count
10
10
 
11
11
  # Specifies the `port` that Puma will listen on to receive requests; default is 3000.
12
12
  #
13
- port ENV.fetch("PORT") { 3000 }
13
+ port ENV.fetch('PORT', 3000)
14
14
 
15
15
  # Specifies the `environment` that Puma will run in.
16
16
  #
17
- environment ENV.fetch("RAILS_ENV") { "development" }
17
+ environment ENV.fetch('RAILS_ENV') { 'development' }
18
18
 
19
19
  # Specifies the `pidfile` that Puma will use.
20
- pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
20
+ pidfile ENV.fetch('PIDFILE') { 'tmp/pids/server.pid' }
21
21
 
22
22
  # Specifies the number of `workers` to boot in clustered mode.
23
23
  # Workers are forked web server processes. If using threads and workers together
@@ -1,6 +1,9 @@
1
1
  Rails.application.routes.draw do
2
+ resources :posts
2
3
  resources :people do
3
4
  get :swagger, on: :collection
4
5
  end
6
+
7
+ mount SoberSwag::Server.new(cache: -> { Rails.env.production? }), at: '/swagger'
5
8
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
6
9
  end
@@ -1,6 +1,6 @@
1
1
  Spring.watch(
2
- ".ruby-version",
3
- ".rbenv-vars",
4
- "tmp/restart.txt",
5
- "tmp/caching-dev.txt"
2
+ '.ruby-version',
3
+ '.rbenv-vars',
4
+ 'tmp/restart.txt',
5
+ 'tmp/caching-dev.txt'
6
6
  )
@@ -1,5 +1,4 @@
1
1
  class CreatePeople < ActiveRecord::Migration[6.0]
2
-
3
2
  def change
4
3
  create_table :people do |t|
5
4
  t.text :first_name, null: false
@@ -0,0 +1,11 @@
1
+ class CreatePosts < ActiveRecord::Migration[6.0]
2
+ def change
3
+ create_table :posts do |t|
4
+ t.references :person, null: false, foreign_key: true, index: true
5
+ t.text :title, null: false
6
+ t.text :body, null: false
7
+
8
+ t.timestamps
9
+ end
10
+ end
11
+ end
@@ -10,14 +10,23 @@
10
10
  #
11
11
  # It's strongly recommended that you check this file into your version control system.
12
12
 
13
- ActiveRecord::Schema.define(version: 2020_03_11_152021) do
13
+ ActiveRecord::Schema.define(version: 20_200_603_172_347) do
14
+ create_table 'people', force: :cascade do |t|
15
+ t.text 'first_name', null: false
16
+ t.text 'last_name', null: false
17
+ t.datetime 'date_of_birth'
18
+ t.datetime 'created_at', precision: 6, null: false
19
+ t.datetime 'updated_at', precision: 6, null: false
20
+ end
14
21
 
15
- create_table "people", force: :cascade do |t|
16
- t.text "first_name", null: false
17
- t.text "last_name", null: false
18
- t.datetime "date_of_birth"
19
- t.datetime "created_at", precision: 6, null: false
20
- t.datetime "updated_at", precision: 6, null: false
22
+ create_table 'posts', force: :cascade do |t|
23
+ t.integer 'person_id', null: false
24
+ t.text 'title', null: false
25
+ t.text 'body', null: false
26
+ t.datetime 'created_at', precision: 6, null: false
27
+ t.datetime 'updated_at', precision: 6, null: false
28
+ t.index ['person_id'], name: 'index_posts_on_person_id'
21
29
  end
22
30
 
31
+ add_foreign_key 'posts', 'people'
23
32
  end
@@ -0,0 +1,64 @@
1
+ # This file is copied to spec/ when you run 'rails generate rspec:install'
2
+ require 'spec_helper'
3
+ ENV['RAILS_ENV'] ||= 'test'
4
+ require File.expand_path('../config/environment', __dir__)
5
+ # Prevent database truncation if the environment is production
6
+ abort('The Rails environment is running in production mode!') if Rails.env.production?
7
+ require 'rspec/rails'
8
+ # Add additional requires below this line. Rails is not loaded until this point!
9
+
10
+ # Requires supporting ruby files with custom matchers and macros, etc, in
11
+ # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
12
+ # run as spec files by default. This means that files in spec/support that end
13
+ # in _spec.rb will both be required and run as specs, causing the specs to be
14
+ # run twice. It is recommended that you do not name files matching this glob to
15
+ # end with _spec.rb. You can configure this pattern with the --pattern
16
+ # option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
17
+ #
18
+ # The following line is provided for convenience purposes. It has the downside
19
+ # of increasing the boot-up time by auto-requiring all files in the support
20
+ # directory. Alternatively, in the individual `*_spec.rb` files, manually
21
+ # require only the support files necessary.
22
+ #
23
+ # Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }
24
+
25
+ # Checks for pending migrations and applies them before tests are run.
26
+ # If you are not using ActiveRecord, you can remove these lines.
27
+ begin
28
+ ActiveRecord::Migration.maintain_test_schema!
29
+ rescue ActiveRecord::PendingMigrationError => e
30
+ puts e.to_s.strip
31
+ exit 1
32
+ end
33
+ RSpec.configure do |config|
34
+ # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
35
+ config.fixture_path = "#{::Rails.root}/spec/fixtures"
36
+
37
+ # If you're not using ActiveRecord, or you'd prefer not to run each of your
38
+ # examples within a transaction, remove the following line or assign false
39
+ # instead of true.
40
+ config.use_transactional_fixtures = true
41
+
42
+ # You can uncomment this line to turn off ActiveRecord support entirely.
43
+ # config.use_active_record = false
44
+
45
+ # RSpec Rails can automatically mix in different behaviours to your tests
46
+ # based on their file location, for example enabling you to call `get` and
47
+ # `post` in specs under `spec/controllers`.
48
+ #
49
+ # You can disable this behaviour by removing the line below, and instead
50
+ # explicitly tag your specs with their type, e.g.:
51
+ #
52
+ # RSpec.describe UsersController, type: :controller do
53
+ # # ...
54
+ # end
55
+ #
56
+ # The different available types are documented in the features, such as in
57
+ # https://relishapp.com/rspec/rspec-rails/docs
58
+ config.infer_spec_type_from_file_location!
59
+
60
+ # Filter lines from Rails gems in backtraces.
61
+ config.filter_rails_from_backtrace!
62
+ # arbitrary gems may also be filtered via:
63
+ # config.filter_gems_from_backtrace("gem name")
64
+ end
@@ -0,0 +1,52 @@
1
+ require 'rails_helper'
2
+
3
+ RSpec.describe 'people controller create', type: :request do
4
+ let(:request) { post '/people', params: params }
5
+
6
+ context 'with good params' do
7
+ let(:params) { { person: { first_name: 'Anthony', last_name: 'Guy' } } }
8
+
9
+ describe 'the effects of the request' do
10
+ subject { proc { request } }
11
+
12
+ it { should change(Person, :count).by(1) }
13
+ it { should change(Person.where(first_name: 'Anthony'), :count).by(1) }
14
+ end
15
+
16
+ describe 'the response' do
17
+ it 'is successful' do
18
+ request
19
+ expect(response).to be_successful
20
+ end
21
+
22
+ it 'returns the person' do
23
+ request
24
+ expect(JSON.parse(response.body)).to include('first_name' => 'Anthony')
25
+ end
26
+ end
27
+ end
28
+
29
+ context 'with bad params' do
30
+ let(:params) { { person: { first_name: '', last_name: '' } } }
31
+
32
+ describe 'the response' do
33
+ subject { request && response }
34
+
35
+ it { should_not be_successful }
36
+ it { should_not be_server_error }
37
+ it { should be_unprocessable }
38
+ end
39
+
40
+ describe 'the act of requesting' do
41
+ subject { proc { request } }
42
+
43
+ it { should_not change(Person, :count) }
44
+ end
45
+
46
+ describe 'the response body' do
47
+ subject { request && response && JSON.parse(response.body) }
48
+
49
+ it { should have_key('first_name') }
50
+ end
51
+ end
52
+ end