sober_swag 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/config/rubocop_linter_action.yml +5 -0
- data/.github/workflows/lint.yml +15 -0
- data/.github/workflows/ruby.yml +23 -1
- data/.gitignore +3 -0
- data/.rubocop.yml +73 -1
- data/.ruby-version +1 -1
- data/Gemfile.lock +29 -5
- data/README.md +109 -0
- data/bin/console +15 -14
- data/docs/serializers.md +203 -0
- data/example/.rspec +1 -0
- data/example/.ruby-version +1 -1
- data/example/Gemfile +10 -6
- data/example/Gemfile.lock +96 -76
- data/example/app/controllers/people_controller.rb +37 -21
- data/example/app/controllers/posts_controller.rb +102 -0
- data/example/app/models/application_record.rb +3 -0
- data/example/app/models/person.rb +6 -0
- data/example/app/models/post.rb +9 -0
- data/example/app/output_objects/person_errors_output_object.rb +5 -0
- data/example/app/output_objects/person_output_object.rb +15 -0
- data/example/app/output_objects/post_output_object.rb +10 -0
- data/example/bin/bundle +24 -20
- data/example/bin/rails +1 -1
- data/example/bin/rake +1 -1
- data/example/config/application.rb +11 -7
- data/example/config/environments/development.rb +0 -1
- data/example/config/environments/production.rb +3 -3
- data/example/config/puma.rb +5 -5
- data/example/config/routes.rb +3 -0
- data/example/config/spring.rb +4 -4
- data/example/db/migrate/20200311152021_create_people.rb +0 -1
- data/example/db/migrate/20200603172347_create_posts.rb +11 -0
- data/example/db/schema.rb +16 -7
- data/example/spec/rails_helper.rb +64 -0
- data/example/spec/requests/people/create_spec.rb +52 -0
- data/example/spec/requests/people/get_spec.rb +35 -0
- data/example/spec/requests/people/index_spec.rb +69 -0
- data/example/spec/spec_helper.rb +94 -0
- data/lib/sober_swag.rb +6 -3
- data/lib/sober_swag/compiler/error.rb +2 -0
- data/lib/sober_swag/compiler/path.rb +2 -5
- data/lib/sober_swag/compiler/paths.rb +0 -1
- data/lib/sober_swag/compiler/type.rb +28 -15
- data/lib/sober_swag/controller.rb +16 -11
- data/lib/sober_swag/controller/route.rb +18 -21
- data/lib/sober_swag/controller/undefined_body_error.rb +3 -0
- data/lib/sober_swag/controller/undefined_path_error.rb +3 -0
- data/lib/sober_swag/controller/undefined_query_error.rb +3 -0
- data/lib/sober_swag/input_object.rb +28 -0
- data/lib/sober_swag/nodes/array.rb +1 -1
- data/lib/sober_swag/nodes/base.rb +2 -4
- data/lib/sober_swag/nodes/binary.rb +2 -1
- data/lib/sober_swag/nodes/enum.rb +4 -2
- data/lib/sober_swag/nodes/list.rb +0 -1
- data/lib/sober_swag/nodes/primitive.rb +6 -5
- data/lib/sober_swag/output_object.rb +102 -0
- data/lib/sober_swag/output_object/definition.rb +30 -0
- data/lib/sober_swag/{blueprint → output_object}/field.rb +14 -4
- data/lib/sober_swag/{blueprint → output_object}/field_syntax.rb +1 -1
- data/lib/sober_swag/{blueprint → output_object}/view.rb +15 -6
- data/lib/sober_swag/parser.rb +5 -3
- data/lib/sober_swag/serializer.rb +5 -2
- data/lib/sober_swag/serializer/array.rb +12 -0
- data/lib/sober_swag/serializer/base.rb +50 -1
- data/lib/sober_swag/serializer/conditional.rb +15 -2
- data/lib/sober_swag/serializer/field_list.rb +29 -6
- data/lib/sober_swag/serializer/mapped.rb +12 -2
- data/lib/sober_swag/serializer/meta.rb +35 -0
- data/lib/sober_swag/serializer/optional.rb +17 -2
- data/lib/sober_swag/serializer/primitive.rb +4 -1
- data/lib/sober_swag/server.rb +83 -0
- data/lib/sober_swag/types.rb +3 -0
- data/lib/sober_swag/version.rb +1 -1
- data/sober_swag.gemspec +6 -4
- metadata +77 -44
- data/example/person.json +0 -4
- data/example/test/controllers/.keep +0 -0
- data/example/test/fixtures/.keep +0 -0
- data/example/test/fixtures/files/.keep +0 -0
- data/example/test/fixtures/people.yml +0 -11
- data/example/test/integration/.keep +0 -0
- data/example/test/models/.keep +0 -0
- data/example/test/models/person_test.rb +0 -7
- data/example/test/test_helper.rb +0 -13
- data/lib/sober_swag/blueprint.rb +0 -113
- data/lib/sober_swag/path.rb +0 -8
- data/lib/sober_swag/path/integer.rb +0 -21
- data/lib/sober_swag/path/lit.rb +0 -41
- data/lib/sober_swag/path/literal.rb +0 -29
- data/lib/sober_swag/path/param.rb +0 -33
@@ -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
|
data/example/bin/bundle
CHANGED
@@ -8,7 +8,7 @@
|
|
8
8
|
# this file is here to facilitate running it.
|
9
9
|
#
|
10
10
|
|
11
|
-
require
|
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[
|
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
|
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
|
-
|
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[
|
41
|
+
gemfile = ENV['BUNDLE_GEMFILE']
|
42
42
|
return gemfile if gemfile && !gemfile.empty?
|
43
43
|
|
44
|
-
File.expand_path(
|
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
|
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
|
-
|
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(
|
78
|
+
return requirement unless Gem::Version.new(Gem::VERSION) < Gem::Version.new('2.7.0')
|
77
79
|
|
78
|
-
requirement +=
|
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[
|
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
|
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
|
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
|
-
|
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?
|
data/example/bin/rails
CHANGED
data/example/bin/rake
CHANGED
@@ -1,29 +1,33 @@
|
|
1
1
|
require_relative 'boot'
|
2
2
|
|
3
|
-
require
|
3
|
+
require 'rails'
|
4
4
|
# Pick the frameworks you want:
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
5
|
+
require 'active_model/railtie'
|
6
|
+
require 'active_job/railtie'
|
7
|
+
require 'active_record/railtie'
|
8
8
|
# require "active_storage/engine"
|
9
|
-
require
|
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
|
13
|
+
require 'action_view/railtie'
|
14
14
|
# require "action_cable/engine"
|
15
15
|
# require "sprockets/railtie"
|
16
|
-
require
|
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
|
@@ -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
|
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 = [
|
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[
|
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)
|
data/example/config/puma.rb
CHANGED
@@ -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(
|
8
|
-
min_threads_count = ENV.fetch(
|
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(
|
13
|
+
port ENV.fetch('PORT', 3000)
|
14
14
|
|
15
15
|
# Specifies the `environment` that Puma will run in.
|
16
16
|
#
|
17
|
-
environment ENV.fetch(
|
17
|
+
environment ENV.fetch('RAILS_ENV') { 'development' }
|
18
18
|
|
19
19
|
# Specifies the `pidfile` that Puma will use.
|
20
|
-
pidfile ENV.fetch(
|
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
|
data/example/config/routes.rb
CHANGED
@@ -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
|
data/example/config/spring.rb
CHANGED
data/example/db/schema.rb
CHANGED
@@ -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:
|
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
|
16
|
-
t.
|
17
|
-
t.text
|
18
|
-
t.
|
19
|
-
t.datetime
|
20
|
-
t.datetime
|
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
|