grape 1.3.1 → 1.5.1

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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +82 -1
  3. data/LICENSE +1 -1
  4. data/README.md +104 -21
  5. data/UPGRADING.md +265 -39
  6. data/lib/grape.rb +2 -2
  7. data/lib/grape/api.rb +2 -2
  8. data/lib/grape/api/instance.rb +29 -28
  9. data/lib/grape/dsl/helpers.rb +1 -0
  10. data/lib/grape/dsl/inside_route.rb +69 -36
  11. data/lib/grape/dsl/parameters.rb +7 -3
  12. data/lib/grape/dsl/routing.rb +2 -4
  13. data/lib/grape/dsl/validations.rb +18 -1
  14. data/lib/grape/eager_load.rb +1 -1
  15. data/lib/grape/endpoint.rb +8 -6
  16. data/lib/grape/http/headers.rb +1 -0
  17. data/lib/grape/middleware/base.rb +2 -1
  18. data/lib/grape/middleware/error.rb +10 -12
  19. data/lib/grape/middleware/formatter.rb +3 -3
  20. data/lib/grape/middleware/stack.rb +21 -8
  21. data/lib/grape/middleware/versioner/header.rb +1 -1
  22. data/lib/grape/middleware/versioner/parse_media_type_patch.rb +2 -1
  23. data/lib/grape/path.rb +2 -2
  24. data/lib/grape/request.rb +1 -1
  25. data/lib/grape/router.rb +30 -43
  26. data/lib/grape/router/attribute_translator.rb +26 -5
  27. data/lib/grape/router/route.rb +3 -22
  28. data/lib/grape/{serve_file → serve_stream}/file_body.rb +1 -1
  29. data/lib/grape/{serve_file → serve_stream}/sendfile_response.rb +1 -1
  30. data/lib/grape/{serve_file/file_response.rb → serve_stream/stream_response.rb} +8 -8
  31. data/lib/grape/util/base_inheritable.rb +11 -8
  32. data/lib/grape/util/lazy_value.rb +1 -0
  33. data/lib/grape/util/reverse_stackable_values.rb +3 -1
  34. data/lib/grape/util/stackable_values.rb +3 -1
  35. data/lib/grape/validations/attributes_iterator.rb +8 -0
  36. data/lib/grape/validations/multiple_attributes_iterator.rb +1 -1
  37. data/lib/grape/validations/params_scope.rb +8 -6
  38. data/lib/grape/validations/single_attribute_iterator.rb +1 -1
  39. data/lib/grape/validations/types.rb +6 -5
  40. data/lib/grape/validations/types/array_coercer.rb +14 -5
  41. data/lib/grape/validations/types/build_coercer.rb +5 -8
  42. data/lib/grape/validations/types/custom_type_coercer.rb +14 -2
  43. data/lib/grape/validations/types/dry_type_coercer.rb +36 -1
  44. data/lib/grape/validations/types/file.rb +15 -13
  45. data/lib/grape/validations/types/json.rb +40 -36
  46. data/lib/grape/validations/types/primitive_coercer.rb +11 -5
  47. data/lib/grape/validations/types/set_coercer.rb +6 -4
  48. data/lib/grape/validations/types/variant_collection_coercer.rb +1 -1
  49. data/lib/grape/validations/validator_factory.rb +1 -1
  50. data/lib/grape/validations/validators/as.rb +1 -1
  51. data/lib/grape/validations/validators/base.rb +4 -5
  52. data/lib/grape/validations/validators/coerce.rb +3 -10
  53. data/lib/grape/validations/validators/default.rb +3 -5
  54. data/lib/grape/validations/validators/except_values.rb +1 -1
  55. data/lib/grape/validations/validators/multiple_params_base.rb +2 -1
  56. data/lib/grape/validations/validators/regexp.rb +1 -1
  57. data/lib/grape/validations/validators/values.rb +1 -1
  58. data/lib/grape/version.rb +1 -1
  59. data/spec/grape/api/instance_spec.rb +50 -0
  60. data/spec/grape/api_spec.rb +75 -0
  61. data/spec/grape/dsl/inside_route_spec.rb +182 -33
  62. data/spec/grape/endpoint/declared_spec.rb +601 -0
  63. data/spec/grape/endpoint_spec.rb +0 -521
  64. data/spec/grape/entity_spec.rb +6 -0
  65. data/spec/grape/integration/rack_sendfile_spec.rb +12 -8
  66. data/spec/grape/middleware/auth/strategies_spec.rb +1 -1
  67. data/spec/grape/middleware/error_spec.rb +1 -1
  68. data/spec/grape/middleware/formatter_spec.rb +1 -1
  69. data/spec/grape/middleware/stack_spec.rb +3 -1
  70. data/spec/grape/path_spec.rb +4 -4
  71. data/spec/grape/validations/multiple_attributes_iterator_spec.rb +13 -3
  72. data/spec/grape/validations/params_scope_spec.rb +26 -0
  73. data/spec/grape/validations/single_attribute_iterator_spec.rb +17 -6
  74. data/spec/grape/validations/types/array_coercer_spec.rb +35 -0
  75. data/spec/grape/validations/types/primitive_coercer_spec.rb +65 -5
  76. data/spec/grape/validations/types/set_coercer_spec.rb +34 -0
  77. data/spec/grape/validations/types_spec.rb +1 -1
  78. data/spec/grape/validations/validators/coerce_spec.rb +317 -29
  79. data/spec/grape/validations/validators/default_spec.rb +170 -0
  80. data/spec/grape/validations/validators/except_values_spec.rb +1 -0
  81. data/spec/grape/validations/validators/values_spec.rb +1 -1
  82. data/spec/grape/validations_spec.rb +290 -18
  83. data/spec/integration/eager_load/eager_load_spec.rb +15 -0
  84. data/spec/spec_helper.rb +0 -10
  85. data/spec/support/chunks.rb +14 -0
  86. data/spec/support/versioned_helpers.rb +3 -5
  87. metadata +18 -8
@@ -8,6 +8,7 @@ describe Grape::Validations::ExceptValuesValidator do
8
8
  DEFAULT_EXCEPTS = ['invalid-type1', 'invalid-type2', 'invalid-type3'].freeze
9
9
  class << self
10
10
  attr_accessor :excepts
11
+
11
12
  def excepts
12
13
  @excepts ||= []
13
14
  [DEFAULT_EXCEPTS + @excepts].flatten.uniq
@@ -319,7 +319,7 @@ describe Grape::Validations::ValuesValidator do
319
319
  expect(last_response.status).to eq 200
320
320
  end
321
321
 
322
- it 'allows for an optional param with a list of values' do
322
+ it 'accepts for an optional param with a list of values' do
323
323
  put('/optional_with_array_of_string_values', optional: nil)
324
324
  expect(last_response.status).to eq 200
325
325
  end
@@ -9,6 +9,10 @@ describe Grape::Validations do
9
9
  subject
10
10
  end
11
11
 
12
+ def declared_params
13
+ subject.namespace_stackable(:declared_params).flatten
14
+ end
15
+
12
16
  describe 'params' do
13
17
  context 'optional' do
14
18
  before do
@@ -41,7 +45,7 @@ describe Grape::Validations do
41
45
  subject.params do
42
46
  optional :some_param
43
47
  end
44
- expect(subject.route_setting(:declared_params)).to eq([:some_param])
48
+ expect(declared_params).to eq([:some_param])
45
49
  end
46
50
  end
47
51
 
@@ -61,7 +65,7 @@ describe Grape::Validations do
61
65
 
62
66
  it 'adds entity documentation to declared params' do
63
67
  define_optional_using
64
- expect(subject.route_setting(:declared_params)).to eq(%i[field_a field_b])
68
+ expect(declared_params).to eq(%i[field_a field_b])
65
69
  end
66
70
 
67
71
  it 'works when field_a and field_b are not present' do
@@ -108,7 +112,7 @@ describe Grape::Validations do
108
112
  subject.params do
109
113
  requires :some_param
110
114
  end
111
- expect(subject.route_setting(:declared_params)).to eq([:some_param])
115
+ expect(declared_params).to eq([:some_param])
112
116
  end
113
117
 
114
118
  it 'works when required field is present but nil' do
@@ -193,7 +197,7 @@ describe Grape::Validations do
193
197
 
194
198
  it 'adds entity documentation to declared params' do
195
199
  define_requires_all
196
- expect(subject.route_setting(:declared_params)).to eq(%i[required_field optional_field])
200
+ expect(declared_params).to eq(%i[required_field optional_field])
197
201
  end
198
202
 
199
203
  it 'errors when required_field is not present' do
@@ -228,7 +232,7 @@ describe Grape::Validations do
228
232
 
229
233
  it 'adds entity documentation to declared params' do
230
234
  define_requires_none
231
- expect(subject.route_setting(:declared_params)).to eq(%i[required_field optional_field])
235
+ expect(declared_params).to eq(%i[required_field optional_field])
232
236
  end
233
237
 
234
238
  it 'errors when required_field is not present' do
@@ -258,7 +262,7 @@ describe Grape::Validations do
258
262
 
259
263
  it 'adds only the entity documentation to declared params, nothing more' do
260
264
  define_requires_all
261
- expect(subject.route_setting(:declared_params)).to eq(%i[required_field optional_field])
265
+ expect(declared_params).to eq(%i[required_field optional_field])
262
266
  end
263
267
  end
264
268
 
@@ -324,7 +328,7 @@ describe Grape::Validations do
324
328
  requires :key
325
329
  end
326
330
  end
327
- expect(subject.route_setting(:declared_params)).to eq([items: [:key]])
331
+ expect(declared_params).to eq([items: [:key]])
328
332
  end
329
333
  end
330
334
 
@@ -396,7 +400,7 @@ describe Grape::Validations do
396
400
  requires :key
397
401
  end
398
402
  end
399
- expect(subject.route_setting(:declared_params)).to eq([items: [:key]])
403
+ expect(declared_params).to eq([items: [:key]])
400
404
  end
401
405
  end
402
406
 
@@ -459,7 +463,7 @@ describe Grape::Validations do
459
463
  requires :key
460
464
  end
461
465
  end
462
- expect(subject.route_setting(:declared_params)).to eq([items: [:key]])
466
+ expect(declared_params).to eq([items: [:key]])
463
467
  end
464
468
  end
465
469
 
@@ -574,7 +578,7 @@ describe Grape::Validations do
574
578
  # NOTE: with body parameters in json or XML or similar this
575
579
  # should actually fail with: children[parents][name] is missing.
576
580
  expect(last_response.status).to eq(400)
577
- expect(last_response.body).to eq('children[1][parents] is missing')
581
+ expect(last_response.body).to eq('children[1][parents] is missing, children[0][parents][1][name] is missing, children[0][parents][1][name] is empty')
578
582
  end
579
583
 
580
584
  it 'errors when a parameter is not present in array within array' do
@@ -615,7 +619,7 @@ describe Grape::Validations do
615
619
 
616
620
  get '/within_array', children: [name: 'Jay']
617
621
  expect(last_response.status).to eq(400)
618
- expect(last_response.body).to eq('children[0][parents] is missing')
622
+ expect(last_response.body).to eq('children[0][parents] is missing, children[0][parents][0][name] is missing, children[0][parents][0][name] is empty')
619
623
  end
620
624
 
621
625
  it 'errors when param is not an Array' do
@@ -763,7 +767,7 @@ describe Grape::Validations do
763
767
  expect(last_response.status).to eq(200)
764
768
  put_with_json '/within_array', children: [name: 'Jay']
765
769
  expect(last_response.status).to eq(400)
766
- expect(last_response.body).to eq('children[0][parents] is missing')
770
+ expect(last_response.body).to eq('children[0][parents] is missing, children[0][parents][0][name] is missing')
767
771
  end
768
772
  end
769
773
 
@@ -813,7 +817,7 @@ describe Grape::Validations do
813
817
  requires :key
814
818
  end
815
819
  end
816
- expect(subject.route_setting(:declared_params)).to eq([items: [:key]])
820
+ expect(declared_params).to eq([items: [:key]])
817
821
  end
818
822
  end
819
823
 
@@ -838,7 +842,7 @@ describe Grape::Validations do
838
842
  it 'does internal validations if the outer group is present' do
839
843
  get '/nested_optional_group', items: [{ key: 'foo' }]
840
844
  expect(last_response.status).to eq(400)
841
- expect(last_response.body).to eq('items[0][required_subitems] is missing')
845
+ expect(last_response.body).to eq('items[0][required_subitems] is missing, items[0][required_subitems][0][value] is missing')
842
846
 
843
847
  get '/nested_optional_group', items: [{ key: 'foo', required_subitems: [{ value: 'bar' }] }]
844
848
  expect(last_response.status).to eq(200)
@@ -858,7 +862,7 @@ describe Grape::Validations do
858
862
  it 'handles validation within arrays' do
859
863
  get '/nested_optional_group', items: [{ key: 'foo' }]
860
864
  expect(last_response.status).to eq(400)
861
- expect(last_response.body).to eq('items[0][required_subitems] is missing')
865
+ expect(last_response.body).to eq('items[0][required_subitems] is missing, items[0][required_subitems][0][value] is missing')
862
866
 
863
867
  get '/nested_optional_group', items: [{ key: 'foo', required_subitems: [{ value: 'bar' }] }]
864
868
  expect(last_response.status).to eq(200)
@@ -877,7 +881,275 @@ describe Grape::Validations do
877
881
  requires(:required_subitems, type: Array) { requires :value }
878
882
  end
879
883
  end
880
- expect(subject.route_setting(:declared_params)).to eq([items: [:key, { optional_subitems: [:value] }, { required_subitems: [:value] }]])
884
+ expect(declared_params).to eq([items: [:key, { optional_subitems: [:value] }, { required_subitems: [:value] }]])
885
+ end
886
+
887
+ context <<~DESC do
888
+ Issue occurs whenever:
889
+ * param structure with at least three levels
890
+ * 1st level item is a required Array that has >1 entry with an optional item present and >1 entry with an optional item missing
891
+ * 2nd level is an optional Array or Hash
892
+ * 3rd level is a required item (can be any type)
893
+ * additional levels do not effect the issue from occuring
894
+ DESC
895
+
896
+ it "example based off actual real world use case" do
897
+ subject.params do
898
+ requires :orders, type: Array do
899
+ requires :id, type: Integer
900
+ optional :drugs, type: Array do
901
+ requires :batches, type: Array do
902
+ requires :batch_no, type: String
903
+ end
904
+ end
905
+ end
906
+ end
907
+
908
+ subject.get '/validate_required_arrays_under_optional_arrays' do
909
+ 'validate_required_arrays_under_optional_arrays works!'
910
+ end
911
+
912
+ data = {
913
+ orders: [
914
+ { id: 77, drugs: [{batches: [{batch_no: "A1234567"}]}]},
915
+ { id: 70 }
916
+ ]
917
+ }
918
+
919
+ get '/validate_required_arrays_under_optional_arrays', data
920
+ expect(last_response.body).to eq("validate_required_arrays_under_optional_arrays works!")
921
+ expect(last_response.status).to eq(200)
922
+ end
923
+
924
+ it "simplest example using Arry -> Array -> Hash -> String" do
925
+ subject.params do
926
+ requires :orders, type: Array do
927
+ requires :id, type: Integer
928
+ optional :drugs, type: Array do
929
+ requires :batch_no, type: String
930
+ end
931
+ end
932
+ end
933
+
934
+ subject.get '/validate_required_arrays_under_optional_arrays' do
935
+ 'validate_required_arrays_under_optional_arrays works!'
936
+ end
937
+
938
+ data = {
939
+ orders: [
940
+ { id: 77, drugs: [{batch_no: "A1234567"}]},
941
+ { id: 70 }
942
+ ]
943
+ }
944
+
945
+ get '/validate_required_arrays_under_optional_arrays', data
946
+ expect(last_response.body).to eq("validate_required_arrays_under_optional_arrays works!")
947
+ expect(last_response.status).to eq(200)
948
+ end
949
+
950
+ it "simplest example using Arry -> Hash -> String" do
951
+ subject.params do
952
+ requires :orders, type: Array do
953
+ requires :id, type: Integer
954
+ optional :drugs, type: Hash do
955
+ requires :batch_no, type: String
956
+ end
957
+ end
958
+ end
959
+
960
+ subject.get '/validate_required_arrays_under_optional_arrays' do
961
+ 'validate_required_arrays_under_optional_arrays works!'
962
+ end
963
+
964
+ data = {
965
+ orders: [
966
+ { id: 77, drugs: {batch_no: "A1234567"}},
967
+ { id: 70 }
968
+ ]
969
+ }
970
+
971
+ get '/validate_required_arrays_under_optional_arrays', data
972
+ expect(last_response.body).to eq("validate_required_arrays_under_optional_arrays works!")
973
+ expect(last_response.status).to eq(200)
974
+ end
975
+
976
+ it "correctly indexes invalida data" do
977
+ subject.params do
978
+ requires :orders, type: Array do
979
+ requires :id, type: Integer
980
+ optional :drugs, type: Array do
981
+ requires :batch_no, type: String
982
+ requires :quantity, type: Integer
983
+ end
984
+ end
985
+ end
986
+
987
+ subject.get '/correctly_indexes' do
988
+ 'correctly_indexes works!'
989
+ end
990
+
991
+ data = {
992
+ orders: [
993
+ { id: 70 },
994
+ { id: 77, drugs: [{batch_no: "A1234567", quantity: 12}, {batch_no: "B222222"}]}
995
+ ]
996
+ }
997
+
998
+ get '/correctly_indexes', data
999
+ expect(last_response.body).to eq("orders[1][drugs][1][quantity] is missing")
1000
+ expect(last_response.status).to eq(400)
1001
+ end
1002
+
1003
+ context "multiple levels of optional and requires settings" do
1004
+ before do
1005
+ subject.params do
1006
+ requires :top, type: Array do
1007
+ requires :top_id, type: Integer, allow_blank: false
1008
+ optional :middle_1, type: Array do
1009
+ requires :middle_1_id, type: Integer, allow_blank: false
1010
+ optional :middle_2, type: Array do
1011
+ requires :middle_2_id, type: String, allow_blank: false
1012
+ optional :bottom, type: Array do
1013
+ requires :bottom_id, type: Integer, allow_blank: false
1014
+ end
1015
+ end
1016
+ end
1017
+ end
1018
+ end
1019
+
1020
+ subject.get '/multi_level' do
1021
+ 'multi_level works!'
1022
+ end
1023
+ end
1024
+
1025
+ it "with valid data" do
1026
+ data = {
1027
+ top: [
1028
+ { top_id: 1, middle_1: [
1029
+ {middle_1_id: 11}, {middle_1_id: 12, middle_2: [
1030
+ {middle_2_id: 121}, {middle_2_id: 122, bottom: [{bottom_id: 1221}]}]}]},
1031
+ { top_id: 2, middle_1: [
1032
+ {middle_1_id: 21}, {middle_1_id: 22, middle_2: [
1033
+ {middle_2_id: 221}]}]},
1034
+ { top_id: 3, middle_1: [
1035
+ {middle_1_id: 31}, {middle_1_id: 32}]},
1036
+ { top_id: 4 }
1037
+ ]
1038
+ }
1039
+
1040
+ get '/multi_level', data
1041
+ expect(last_response.body).to eq("multi_level works!")
1042
+ expect(last_response.status).to eq(200)
1043
+ end
1044
+
1045
+ it "with invalid data" do
1046
+ data = {
1047
+ top: [
1048
+ { top_id: 1, middle_1: [
1049
+ {middle_1_id: 11}, {middle_1_id: 12, middle_2: [
1050
+ {middle_2_id: 121}, {middle_2_id: 122, bottom: [{bottom_id: nil}]}]}]},
1051
+ { top_id: 2, middle_1: [
1052
+ {middle_1_id: 21}, {middle_1_id: 22, middle_2: [{middle_2_id: nil}]}]},
1053
+ { top_id: 3, middle_1: [
1054
+ {middle_1_id: nil}, {middle_1_id: 32}]},
1055
+ { top_id: nil, missing_top_id: 4 }
1056
+ ]
1057
+ }
1058
+ # debugger
1059
+ get '/multi_level', data
1060
+ expect(last_response.body.split(", ")).to match_array([
1061
+ "top[3][top_id] is empty",
1062
+ "top[2][middle_1][0][middle_1_id] is empty",
1063
+ "top[1][middle_1][1][middle_2][0][middle_2_id] is empty",
1064
+ "top[0][middle_1][1][middle_2][1][bottom][0][bottom_id] is empty"
1065
+ ])
1066
+ expect(last_response.status).to eq(400)
1067
+ end
1068
+ end
1069
+ end
1070
+
1071
+ it "exactly_one_of" do
1072
+ subject.params do
1073
+ requires :orders, type: Array do
1074
+ requires :id, type: Integer
1075
+ optional :drugs, type: Hash do
1076
+ optional :batch_no, type: String
1077
+ optional :batch_id, type: String
1078
+ exactly_one_of :batch_no, :batch_id
1079
+ end
1080
+ end
1081
+ end
1082
+
1083
+ subject.get '/exactly_one_of' do
1084
+ 'exactly_one_of works!'
1085
+ end
1086
+
1087
+ data = {
1088
+ orders: [
1089
+ { id: 77, drugs: {batch_no: "A1234567"}},
1090
+ { id: 70 }
1091
+ ]
1092
+ }
1093
+
1094
+ get '/exactly_one_of', data
1095
+ expect(last_response.body).to eq("exactly_one_of works!")
1096
+ expect(last_response.status).to eq(200)
1097
+ end
1098
+
1099
+ it "at_least_one_of" do
1100
+ subject.params do
1101
+ requires :orders, type: Array do
1102
+ requires :id, type: Integer
1103
+ optional :drugs, type: Hash do
1104
+ optional :batch_no, type: String
1105
+ optional :batch_id, type: String
1106
+ at_least_one_of :batch_no, :batch_id
1107
+ end
1108
+ end
1109
+ end
1110
+
1111
+ subject.get '/at_least_one_of' do
1112
+ 'at_least_one_of works!'
1113
+ end
1114
+
1115
+ data = {
1116
+ orders: [
1117
+ { id: 77, drugs: {batch_no: "A1234567"}},
1118
+ { id: 70 }
1119
+ ]
1120
+ }
1121
+
1122
+ get '/at_least_one_of', data
1123
+ expect(last_response.body).to eq("at_least_one_of works!")
1124
+ expect(last_response.status).to eq(200)
1125
+ end
1126
+
1127
+ it "all_or_none_of" do
1128
+ subject.params do
1129
+ requires :orders, type: Array do
1130
+ requires :id, type: Integer
1131
+ optional :drugs, type: Hash do
1132
+ optional :batch_no, type: String
1133
+ optional :batch_id, type: String
1134
+ all_or_none_of :batch_no, :batch_id
1135
+ end
1136
+ end
1137
+ end
1138
+
1139
+ subject.get '/all_or_none_of' do
1140
+ 'all_or_none_of works!'
1141
+ end
1142
+
1143
+ data = {
1144
+ orders: [
1145
+ { id: 77, drugs: {batch_no: "A1234567", batch_id: "12"}},
1146
+ { id: 70 }
1147
+ ]
1148
+ }
1149
+
1150
+ get '/all_or_none_of', data
1151
+ expect(last_response.body).to eq("all_or_none_of works!")
1152
+ expect(last_response.status).to eq(200)
881
1153
  end
882
1154
  end
883
1155
 
@@ -1122,14 +1394,14 @@ describe Grape::Validations do
1122
1394
  subject.params do
1123
1395
  use :pagination
1124
1396
  end
1125
- expect(subject.route_setting(:declared_params)).to eq %i[page per_page]
1397
+ expect(declared_params).to eq %i[page per_page]
1126
1398
  end
1127
1399
 
1128
1400
  it 'by #use with multiple params' do
1129
1401
  subject.params do
1130
1402
  use :pagination, :period
1131
1403
  end
1132
- expect(subject.route_setting(:declared_params)).to eq %i[page per_page start_date end_date]
1404
+ expect(declared_params).to eq %i[page per_page start_date end_date]
1133
1405
  end
1134
1406
  end
1135
1407
 
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', '..', 'lib'))
4
+ require 'grape'
5
+
6
+ describe Grape do
7
+ it 'eager_load!' do
8
+ require 'grape/eager_load'
9
+ expect { Grape.eager_load! }.to_not raise_error
10
+ end
11
+
12
+ it 'compile!' do
13
+ expect { Class.new(Grape::API).compile! }.to_not raise_error
14
+ end
15
+ end