raml_ruby 0.1.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 (88) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.travis.yml +6 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +71 -0
  7. data/Rakefile +1 -0
  8. data/fixtures/include_1.raml +7 -0
  9. data/fixtures/schemas/canonicalSchemas.raml +3 -0
  10. data/fixtures/schemas/filesystem/file.json +1 -0
  11. data/fixtures/schemas/filesystem/files.json +1 -0
  12. data/fixtures/schemas/filesystem/fileupdate.json +1 -0
  13. data/fixtures/schemas/filesystem/relative/test.json +1 -0
  14. data/lib/raml.rb +104 -0
  15. data/lib/raml/exceptions.rb +27 -0
  16. data/lib/raml/mixin/bodies.rb +32 -0
  17. data/lib/raml/mixin/documentable.rb +32 -0
  18. data/lib/raml/mixin/global.rb +20 -0
  19. data/lib/raml/mixin/headers.rb +22 -0
  20. data/lib/raml/mixin/merge.rb +24 -0
  21. data/lib/raml/mixin/parent.rb +54 -0
  22. data/lib/raml/mixin/validation.rb +49 -0
  23. data/lib/raml/node.rb +219 -0
  24. data/lib/raml/node/abstract_method.rb +61 -0
  25. data/lib/raml/node/abstract_resource.rb +165 -0
  26. data/lib/raml/node/abstract_resource_circular.rb +5 -0
  27. data/lib/raml/node/body.rb +94 -0
  28. data/lib/raml/node/documentation.rb +28 -0
  29. data/lib/raml/node/header.rb +4 -0
  30. data/lib/raml/node/method.rb +106 -0
  31. data/lib/raml/node/parameter/abstract_parameter.rb +251 -0
  32. data/lib/raml/node/parameter/base_uri_parameter.rb +6 -0
  33. data/lib/raml/node/parameter/form_parameter.rb +6 -0
  34. data/lib/raml/node/parameter/query_parameter.rb +6 -0
  35. data/lib/raml/node/parameter/uri_parameter.rb +7 -0
  36. data/lib/raml/node/parametized_reference.rb +15 -0
  37. data/lib/raml/node/reference.rb +4 -0
  38. data/lib/raml/node/resource.rb +26 -0
  39. data/lib/raml/node/resource_type.rb +20 -0
  40. data/lib/raml/node/resource_type_reference.rb +5 -0
  41. data/lib/raml/node/response.rb +32 -0
  42. data/lib/raml/node/root.rb +246 -0
  43. data/lib/raml/node/schema.rb +41 -0
  44. data/lib/raml/node/schema_reference.rb +5 -0
  45. data/lib/raml/node/template.rb +55 -0
  46. data/lib/raml/node/trait.rb +18 -0
  47. data/lib/raml/node/trait_reference.rb +5 -0
  48. data/lib/raml/parser.rb +57 -0
  49. data/lib/raml/parser/include.rb +25 -0
  50. data/lib/raml/patch/hash.rb +6 -0
  51. data/lib/raml/patch/module.rb +12 -0
  52. data/lib/raml/version.rb +3 -0
  53. data/raml_ruby.gemspec +35 -0
  54. data/raml_spec_reqs.md +276 -0
  55. data/templates/abstract_parameter.slim +68 -0
  56. data/templates/body.slim +15 -0
  57. data/templates/collapse.slim +10 -0
  58. data/templates/documentation.slim +2 -0
  59. data/templates/method.slim +38 -0
  60. data/templates/resource.slim +33 -0
  61. data/templates/response.slim +13 -0
  62. data/templates/root.slim +39 -0
  63. data/templates/style.sass +119 -0
  64. data/test/apis/box-api.raml +4224 -0
  65. data/test/apis/instagram-api.raml +3378 -0
  66. data/test/apis/stripe-api.raml +12227 -0
  67. data/test/apis/twilio-rest-api.raml +6618 -0
  68. data/test/apis/twitter-rest-api.raml +34284 -0
  69. data/test/raml/body_spec.rb +268 -0
  70. data/test/raml/documentation_spec.rb +49 -0
  71. data/test/raml/header_spec.rb +17 -0
  72. data/test/raml/include_spec.rb +40 -0
  73. data/test/raml/method_spec.rb +701 -0
  74. data/test/raml/parameter/abstract_parameter_spec.rb +564 -0
  75. data/test/raml/parameter/form_parameter_spec.rb +17 -0
  76. data/test/raml/parameter/query_parameter_spec.rb +33 -0
  77. data/test/raml/parameter/uri_parameter_spec.rb +44 -0
  78. data/test/raml/parser_spec.rb +53 -0
  79. data/test/raml/raml_spec.rb +32 -0
  80. data/test/raml/resource_spec.rb +440 -0
  81. data/test/raml/resource_type_spec.rb +51 -0
  82. data/test/raml/response_spec.rb +251 -0
  83. data/test/raml/root_spec.rb +655 -0
  84. data/test/raml/schema_spec.rb +110 -0
  85. data/test/raml/spec_helper.rb +11 -0
  86. data/test/raml/template_spec.rb +98 -0
  87. data/test/raml/trait_spec.rb +31 -0
  88. metadata +337 -0
@@ -0,0 +1,268 @@
1
+ require_relative 'spec_helper'
2
+
3
+ describe Raml::Body do
4
+ let (:media_type) { 'text/xml' }
5
+ let (:body_data ) {
6
+ YAML.load(%q(
7
+ schema: |
8
+ {
9
+ "$schema": "http://json-schema.org/draft-03/schema#",
10
+ "properties": {
11
+ "input": {
12
+ "required": false,
13
+ "type": "string"
14
+ }
15
+ },
16
+ "required": false,
17
+ "type": "object"
18
+ }
19
+ ))
20
+ }
21
+ let(:form_body_data) {
22
+ YAML.load(%q(
23
+ formParameters:
24
+ param:
25
+ type: string
26
+ ))
27
+ }
28
+ let(:root) { Raml::Root.new 'title' => 'x', 'baseUri' => 'http://foo.com', 'schemas' => [{'Job' => 'xxx'}] }
29
+
30
+ subject { Raml::Body.new media_type, body_data, root }
31
+
32
+ describe '#initialize' do
33
+ context 'when the media type is valid' do
34
+ it "inits body with media_type" do
35
+ expect( subject.media_type ).to eq media_type
36
+ end
37
+ end
38
+ context 'when the media type is invalid' do
39
+ let(:media_type) { 'foo' }
40
+ it { expect { subject }.to raise_error Raml::InvalidMediaType }
41
+ end
42
+
43
+ context 'when the body is not a web form' do
44
+ context 'when the schema property is valid schema' do
45
+ it "inits body with schema" do
46
+ expect( subject.schema ).to be_an Raml::Schema
47
+ end
48
+ end
49
+ context 'when the schema property is valid schema reference' do
50
+ let (:body_data ) { { 'schema' => 'Job' } }
51
+ it "inits body with schema" do
52
+ expect( subject.schema ).to be_an Raml::SchemaReference
53
+ end
54
+ end
55
+ context 'when the schema property is not a string' do
56
+ let (:body_data ) { { 'schema' => 1 } }
57
+ it { expect { subject }.to raise_error Raml::InvalidProperty, /schema/ }
58
+ end
59
+ context 'when the schema property is an empty string' do
60
+ let (:body_data ) { { 'schema' => '' } }
61
+ it { expect { subject }.to raise_error Raml::InvalidProperty, /schema/ }
62
+ end
63
+ end
64
+ context 'when body is a web form' do
65
+ let(:body_data) { form_body_data }
66
+ [ 'application/x-www-form-urlencoded', 'multipart/form-data' ].each do |mtype|
67
+ context "when media type is #{mtype}" do
68
+ let(:media_type) { mtype }
69
+ context 'when a formParameters property is not provided' do
70
+ before { body_data.delete 'formParameters' }
71
+ it { expect { subject }.to raise_error Raml::RequiredPropertyMissing, /formParameters/ }
72
+ end
73
+ context 'when a formParameters property is provided' do
74
+ context 'when a formParameters property is valid' do
75
+ it { expect { subject }.to_not raise_error }
76
+ it 'stores all as Raml::Parameter::FormParameter instances' do
77
+ expect( subject.form_parameters.values ).to all( be_a Raml::Parameter::FormParameter )
78
+ subject.form_parameters.keys.should contain_exactly('param')
79
+ end
80
+ end
81
+ context 'when the formParameters property is not a map' do
82
+ before { body_data['formParameters'] = 1 }
83
+ it { expect { subject }.to raise_error Raml::InvalidProperty, /formParameters/ }
84
+ end
85
+ context 'when the formParameters property is not a map with non-string keys' do
86
+ before { body_data['formParameters'] = { 1 => {}} }
87
+ it { expect { subject }.to raise_error Raml::InvalidProperty, /formParameters/ }
88
+ end
89
+ context 'when the formParameters property is not a map with non-string keys' do
90
+ before { body_data['formParameters'] = { '1' => 'x'} }
91
+ it { expect { subject }.to raise_error Raml::InvalidProperty, /formParameters/ }
92
+ end
93
+ end
94
+
95
+ context 'when a schema property is not provided' do
96
+ it { expect { subject }.to_not raise_error }
97
+ end
98
+ context 'when a schema property is provided' do
99
+ let(:body_data) {
100
+ YAML.load %q(
101
+ schema: |
102
+ {
103
+ "$schema": "http://json-schema.org/draft-03/schema#",
104
+ "properties": {
105
+ "input": {
106
+ "required": false,
107
+ "type": "string"
108
+ }
109
+ },
110
+ "required": false,
111
+ "type": "object"
112
+ }
113
+ formParameters:
114
+ param:
115
+ type: string
116
+ )
117
+ }
118
+ it { expect { subject }.to raise_error Raml::InvalidProperty, /schema/ }
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+
125
+ describe '#form_parameters' do
126
+ context 'when body is not a web form' do
127
+ it 'returns no form parameters' do
128
+ subject.form_parameters { should be_empty }
129
+ end
130
+ end
131
+ context 'when body is a web form' do
132
+ let(:body_data) { form_body_data }
133
+ it 'returns form parameters' do
134
+ subject.form_parameters { should_not be_empty }
135
+ expect( subject.form_parameters.values ).to all( be_a Raml::Parameter::FormParameter )
136
+ end
137
+ end
138
+ end
139
+
140
+ describe '#web_form?' do
141
+ context 'when body isnt a web form' do
142
+ it { should_not be_web_form }
143
+ end
144
+ context 'when body is a web form' do
145
+ let(:body_data) { form_body_data }
146
+ [ 'application/x-www-form-urlencoded', 'multipart/form-data' ].each do |mtype|
147
+ context "when media type is #{mtype}" do
148
+ let(:media_type) { mtype }
149
+ it { should be_web_form }
150
+ end
151
+ end
152
+ end
153
+ end
154
+
155
+ describe '#merge' do
156
+ context 'when body and mixin have different media types' do
157
+ let(:body ) { Raml::Body.new 'foo/bar', {}, root }
158
+ let(:mixin) { Raml::Body.new 'foo/boo', {}, root }
159
+ it { expect { body.merge mixin }.to raise_error Raml::MergeError }
160
+ end
161
+ context 'when body and mixin have the same media type' do
162
+ let(:body ) { Raml::Body.new 'foo/bar', body_data , root }
163
+ let(:mixin) { Raml::Body.new 'foo/bar', mixin_data, root }
164
+
165
+ context 'when the body to merge in has a property set' do
166
+ context 'when the body to merge into does not have the property set' do
167
+ let(:body_data) { {} }
168
+ context 'example property' do
169
+ let(:mixin_data) { {'example' => 'body example'} }
170
+ it 'merges in the property' do
171
+ body.merge(mixin).example.should eq mixin.example
172
+ end
173
+ end
174
+ context 'schema property' do
175
+ let(:mixin_data) { {'schema' => 'mixin schema'} }
176
+ it 'merges in the property' do
177
+ body.merge(mixin).schema.should be_a Raml::Schema
178
+ body.schema.should eq mixin.schema
179
+ end
180
+ end
181
+ context 'formParameters properties' do
182
+ let(:mixin_data) { {
183
+ 'formParameters' => {
184
+ 'param1' => {'description' => 'foo'},
185
+ 'param2' => {'description' => 'bar'}
186
+ }
187
+ } }
188
+ it 'adds the form parameters to the body' do
189
+ body.merge(mixin).form_parameters.keys.should contain_exactly('param1', 'param2')
190
+ end
191
+ end
192
+ end
193
+ context 'when the body to merge into has the property set' do
194
+ context 'example property' do
195
+ let(:body_data ) { {'example' => 'body example'} }
196
+ let(:mixin_data) { {'example' => 'mixin example'} }
197
+ it 'overrides the property' do
198
+ body.merge(mixin).example.should eq 'mixin example'
199
+ end
200
+ end
201
+ context 'schema property' do
202
+ let(:body_data ) { {'schema' => 'body schema' } }
203
+ let(:mixin_data) { {'schema' => 'mixin schema'} }
204
+ it 'overrides the property' do
205
+ body.merge(mixin).schema.value.should eq 'mixin schema'
206
+ end
207
+ end
208
+ context 'formParameters properties' do
209
+ let(:body_data) { {
210
+ 'formParameters' => {
211
+ 'param1' => {'description' => 'foo'},
212
+ 'param2' => {'description' => 'bar'}
213
+ }
214
+ } }
215
+ context 'when the merged in body form parameters are different from the form parametes of the body merged into' do
216
+ let(:mixin_data) { {
217
+ 'formParameters' => {
218
+ 'param3' => {'description' => 'foo2'},
219
+ 'param4' => {'description' => 'bar2'}
220
+ }
221
+ } }
222
+ it 'adds the form parameters to the body' do
223
+ body.merge(mixin).form_parameters.keys.should contain_exactly('param1', 'param2', 'param3', 'param4')
224
+ end
225
+ end
226
+ context 'when the merged in body form parameters overlap with the form parametes of the body merged into' do
227
+ let(:mixin_data) { {
228
+ 'formParameters' => {
229
+ 'param2' => {'description' => 'bar3', 'displayName' => 'Param 3'},
230
+ 'param3' => {'description' => 'foo2'},
231
+ 'param4' => {'description' => 'bar2'}
232
+ }
233
+ } }
234
+ it 'merges the matching orm parameters and adds the non-matching orm parameters to the body' do
235
+ body.merge(mixin).form_parameters.keys.should contain_exactly('param1', 'param2', 'param3', 'param4')
236
+ body.form_parameters['param2'].display_name.should eq mixin.form_parameters['param2'].display_name
237
+ body.form_parameters['param2'].description.should eq mixin.form_parameters['param2'].description
238
+ end
239
+ end
240
+ end
241
+ end
242
+ end
243
+ end
244
+ end
245
+
246
+ describe '#document' do
247
+ context 'when the body is not a form body' do
248
+ it 'returns a String' do
249
+ subject.document.should be_a String
250
+ end
251
+ it 'should render the template' do
252
+ mock(Slim::Template).new(/templates\/body.slim\z/, is_a(Hash)).mock!.render(is_a(Raml::Node)) { '' }
253
+ subject.document
254
+ end
255
+ end
256
+ context 'when the body is a form body' do
257
+ let(:media_type) { 'application/x-www-form-urlencoded' }
258
+ let(:body_data) { form_body_data }
259
+ it 'returns a String' do
260
+ subject.document.should be_a String
261
+ end
262
+ it 'should render the template' do
263
+ mock(Slim::Template).new(/templates\/body.slim\z/, is_a(Hash)).mock!.render(is_a(Raml::Node)) { '' }
264
+ subject.document
265
+ end
266
+ end
267
+ end
268
+ end
@@ -0,0 +1,49 @@
1
+ # encoding: UTF-8
2
+ require_relative 'spec_helper'
3
+
4
+ describe Raml::Documentation do
5
+ let(:root_data) { {'title' => 'x', 'baseUri' => 'http://foo.com'} }
6
+ let(:root) { Raml::Root.new root_data }
7
+
8
+ let(:title) { 'Request Formats' }
9
+ let(:content) { <<-EOS
10
+ Credentials
11
+ -----------
12
+
13
+ All requests to Twilio's REST API require you to authenticate using [HTTP basic auth](http://en.wikipedia.org/wiki/Basic_access_authentication) to convey your identity. The username is your AccountSid (a 34 character string, starting with the letters AC). The password is your AuthToken. Your AccountSid and AuthToken are on the [Account Dashboard](http://www.twilio.com/user/account/) page.
14
+
15
+ Most HTTP clients (including web-browsers) present a dialog or prompt for you to provide a username and password for HTTP basic auth. Most clients will also allow you to provide credentials in the URL itself. For example:
16
+
17
+ ```
18
+ https://{AccountSid}:{AuthToken}@api.twilio.com/2010-04-01/Accounts
19
+ ```
20
+
21
+ Retrieving Resources with the HTTP GET Method
22
+ ---------------------------------------------
23
+
24
+ You can retrieve a representation of a resource by GETting its url. The easiest way to do this is to copy and paste a URL into your web browser's address bar.
25
+
26
+ ### Possible GET Response Status Codes
27
+
28
+ * 200 OK: The request was successful and the response body contains the representation requested.
29
+ * 302 FOUND: A common redirect response; you can GET the representation at the URI in the Location response header.
30
+ * 304 NOT MODIFIED: Your client's cached version of the representation is still up to date.
31
+ * 401 UNAUTHORIZED: The supplied credentials, if any, are not sufficient to access the resource.
32
+ * 404 NOT FOUND: You know this one.
33
+ * 429 TOO MANY REQUESTS: Your application is sending too many simultaneous requests.
34
+ * 500 SERVER ERROR: We couldn't return the representation due to an internal server error.
35
+ * 503 SERVICE UNAVAILABLE: We are temporarily unable to return the representation. Please wait for a bit and try again.
36
+ EOS
37
+ }
38
+ subject { Raml::Documentation.new(title, {'content' => content }, root) }
39
+
40
+ describe '#document' do
41
+ it 'returns a String' do
42
+ subject.document.should be_a String
43
+ end
44
+ it 'should render the template' do
45
+ mock(Slim::Template).new(/templates\/documentation.slim\z/, is_a(Hash)).mock!.render(is_a(Raml::Node)) { '' }
46
+ subject.document
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,17 @@
1
+ require_relative 'spec_helper'
2
+
3
+ describe Raml::Header do
4
+ let(:root_data) { {'title' => 'x', 'baseUri' => 'http://foo.com'} }
5
+ let(:root) { Raml::Root.new root_data }
6
+ let(:name) { 'meta' }
7
+ let (:data) {
8
+ YAML.load(%q(
9
+ displayName: Job Metadata
10
+ description: Field names prefixed with x-Zencoder-job-metadata- contain user-specified metadata.
11
+ ))
12
+ }
13
+
14
+ it "should instanciate Header" do
15
+ Raml::Header.new(name, data, root)
16
+ end
17
+ end
@@ -0,0 +1,40 @@
1
+ require_relative 'spec_helper'
2
+ require 'ostruct'
3
+
4
+ describe Raml::Parser::Include do
5
+ let(:include) { Raml::Parser::Include.new }
6
+ describe '#init_with' do
7
+ it 'fetches the path from the given Psych::Coder instance' do
8
+ include.init_with OpenStruct.new scalar: 'path_to_file'
9
+ include.path.should eq 'path_to_file'
10
+ end
11
+ end
12
+
13
+ describe '#content' do
14
+ before { include.init_with OpenStruct.new scalar: path }
15
+ context 'when the path in the include directive is absolute' do
16
+ let(:path) { '/absolute_file_path'}
17
+ it 'opens and reads the file' do
18
+ mock(File).open(path).stub!.read { 'content' }
19
+ include.content('cwd').should eq 'content'
20
+ end
21
+ end
22
+ context 'when the path in the include directive is relative' do
23
+ let(:path) { 'relative_file_path'}
24
+ it 'opens and reads the file' do
25
+ mock(File).open("cwd/#{path}").stub!.read { 'content' }
26
+ include.content('cwd').should eq 'content'
27
+ end
28
+ end
29
+ [ 'yaml', 'yml', 'raml' ].each do |ext|
30
+ context "when include directive points to a file ending in .#{ext}" do
31
+ let(:path) { "/absolute_file_path.#{ext}" }
32
+ let(:content) { { 'a' => [1,2,3], 'b' => 4 } }
33
+ it 'parses the file as YAML' do
34
+ stub(File).open(path).stub!.read { content.to_yaml }
35
+ include.content('cwd').should eq content
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,701 @@
1
+ require_relative 'spec_helper'
2
+
3
+ describe Raml::Method do
4
+ let(:name) { 'get' }
5
+ let(:data) {
6
+ YAML.load(%q(
7
+ description: Get a list of users
8
+ queryParameters:
9
+ page:
10
+ description: Specify the page that you want to retrieve
11
+ type: integer
12
+ required: true
13
+ example: 1
14
+ per_page:
15
+ description: Specify the amount of items that will be retrieved per page
16
+ type: integer
17
+ minimum: 10
18
+ maximum: 200
19
+ default: 30
20
+ example: 50
21
+ protocols: [ HTTP, HTTPS ]
22
+ responses:
23
+ 200:
24
+ description: |
25
+ The list of popular media.
26
+ ))
27
+ }
28
+ let(:root_data) { {'title' => 'x', 'baseUri' => 'http://foo.com'} }
29
+ let(:root) { Raml::Root.new root_data }
30
+
31
+ subject { Raml::Method.new(name, data, root) }
32
+
33
+ describe '#new' do
34
+ it "should instanciate Method" do
35
+ subject
36
+ end
37
+
38
+ context 'when the method is a method defined in RFC2616 or RFC5789' do
39
+ %w(options get head post put delete trace connect patch).each do |method|
40
+ context "when the method is #{method}" do
41
+ let(:name) { method }
42
+ it { expect { subject }.to_not raise_error }
43
+ end
44
+ end
45
+ end
46
+ context 'when the method is an unsupported method' do
47
+ %w(propfind proppatch mkcol copy move lock unlock).each do |method|
48
+ context "when the method is #{method}" do
49
+ let(:name) { method }
50
+ it { expect { subject }.to raise_error Raml::InvalidMethod }
51
+ end
52
+ end
53
+ end
54
+
55
+ context 'when description property is not given' do
56
+ let(:data) { {} }
57
+ it { expect { subject }.to_not raise_error }
58
+ end
59
+ context 'when description property is given' do
60
+ context 'when the description property is not a string' do
61
+ let(:data) { { 'description' => 1 } }
62
+ it { expect { subject }.to raise_error Raml::InvalidProperty, /description/ }
63
+ end
64
+ context 'when the description property is a string' do
65
+ let(:data) { { 'description' => 'My Description' } }
66
+ it { expect { subject }.to_not raise_error }
67
+ it 'should store the value' do
68
+ subject.description.should eq data['description']
69
+ end
70
+ it 'uses the description in the documentation' do
71
+ subject.document.should include data['description']
72
+ end
73
+ end
74
+ end
75
+
76
+ context 'when the headers parameter is given' do
77
+ context 'when the headers property is well formed' do
78
+ let (:data) {
79
+ YAML.load(%q(
80
+ headers:
81
+ Zencoder-Api-Key:
82
+ displayName: ZEncoder API Key
83
+ x-Zencoder-job-metadata-{*}:
84
+ displayName: Job Metadata
85
+ ))
86
+ }
87
+ it { expect { subject }.to_not raise_error }
88
+ it 'stores all as Raml::Header instances' do
89
+ expect( subject.headers.values ).to all( be_a Raml::Header )
90
+ expect( subject.headers.keys ).to contain_exactly('Zencoder-Api-Key','x-Zencoder-job-metadata-{*}')
91
+ end
92
+ end
93
+ context 'when the headers property is not a map' do
94
+ let(:data) { { 'headers' => 1 } }
95
+ it { expect { subject }.to raise_error Raml::InvalidProperty, /headers/ }
96
+ end
97
+ context 'when the headers property is not a map with non-string keys' do
98
+ let(:data) { { 'headers' => { 1 => {}} } }
99
+ it { expect { subject }.to raise_error Raml::InvalidProperty, /headers/ }
100
+ end
101
+ context 'when the headers property is not a map with non-string keys' do
102
+ let(:data) { { 'headers' => { '1' => 'x'} } }
103
+ it { expect { subject }.to raise_error Raml::InvalidProperty, /headers/ }
104
+ end
105
+ end
106
+
107
+ context 'when the protocols property is missing' do
108
+ let(:data) { { } }
109
+ it { expect{ subject }.to_not raise_error }
110
+ end
111
+ context 'when the protocols property is given' do
112
+ context 'when the protocol property is not an array' do
113
+ let(:data) { { 'protocols' => 'HTTP' } }
114
+ it { expect{ subject }.to raise_error Raml::InvalidProperty, /protocols/ }
115
+ end
116
+ context 'when the protocol property is an array but not all elements are strings' do
117
+ let(:data) { { 'protocols' => ['HTTP', 1] } }
118
+ it { expect{ subject }.to raise_error Raml::InvalidProperty, /protocols/ }
119
+ end
120
+ context 'when the protocol property is an array of strings with invalid values' do
121
+ let(:data) { { 'protocols' => ['HTTP', 'foo'] } }
122
+ it { expect{ subject }.to raise_error Raml::InvalidProperty, /protocols/ }
123
+ end
124
+ [
125
+ [ 'HTTP' ],
126
+ [ 'HTTPS' ],
127
+ [ 'HTTP', 'HTTPS' ]
128
+ ].each do |protocols|
129
+ context "when the protocol property is #{protocols}" do
130
+ let(:data) { { 'protocols' => protocols } }
131
+ it { expect{ subject }.to_not raise_error }
132
+ it 'stores the values' do
133
+ subject.protocols.should eq protocols
134
+ end
135
+ end
136
+ end
137
+ context 'when the protocol property is an array of valid values in lowercase' do
138
+ let(:data) { { 'protocols' => ['http', 'https'] } }
139
+ it 'uppercases them' do
140
+ subject.protocols.should eq [ 'HTTP', 'HTTPS' ]
141
+ end
142
+ end
143
+ end
144
+
145
+ context 'when a queryParameters property is given' do
146
+ context 'when the queryParameters property is well formed' do
147
+ let(:data) {
148
+ YAML.load(
149
+ %q(
150
+ description: Get a list of users
151
+ queryParameters:
152
+ page:
153
+ description: Specify the page that you want to retrieve
154
+ type: integer
155
+ required: true
156
+ example: 1
157
+ per_page:
158
+ description: Specify the amount of items that will be retrieved per page
159
+ type: integer
160
+ minimum: 10
161
+ maximum: 200
162
+ default: 30
163
+ example: 50
164
+ )
165
+ )
166
+ }
167
+
168
+ it { expect { subject }.to_not raise_error }
169
+ it 'stores all as Raml::Parameter::UriParameter instances' do
170
+ expect( subject.query_parameters.values ).to all( be_a Raml::Parameter::QueryParameter )
171
+ subject.query_parameters.keys.should contain_exactly('page', 'per_page')
172
+ end
173
+ end
174
+ context 'when the queryParameters property is not a map' do
175
+ before { data['queryParameters'] = 1 }
176
+ it { expect { subject }.to raise_error Raml::InvalidProperty, /queryParameters/ }
177
+ end
178
+ context 'when the queryParameters property is not a map with non-string keys' do
179
+ before { data['queryParameters'] = { 1 => {}} }
180
+ it { expect { subject }.to raise_error Raml::InvalidProperty, /queryParameters/ }
181
+ end
182
+ context 'when the queryParameters property is not a map with non-string keys' do
183
+ before { data['queryParameters'] = { '1' => 'x'} }
184
+ it { expect { subject }.to raise_error Raml::InvalidProperty, /queryParameters/ }
185
+ end
186
+ end
187
+
188
+ context 'when a body property is given' do
189
+ context 'when the body property is well formed' do
190
+ let(:data) {
191
+ YAML.load(
192
+ %q(
193
+ description: Create a Job
194
+ body:
195
+ text/xml:
196
+ schema: job_xml_schema
197
+ application/json:
198
+ schema: json_xml_schema
199
+ )
200
+ )
201
+ }
202
+
203
+ it { expect { subject }.to_not raise_error }
204
+ it 'stores all as Raml::Body instances' do
205
+ expect( subject.bodies.values ).to all( be_a Raml::Body )
206
+ subject.bodies.keys.should contain_exactly('text/xml', 'application/json')
207
+ end
208
+ end
209
+ context 'when the body property is not a map' do
210
+ before { data['body'] = 1 }
211
+ it { expect { subject }.to raise_error Raml::InvalidProperty, /body/ }
212
+ end
213
+ context 'when the body property is a map with non-string keys' do
214
+ before { data['body'] = { 1 => {}} }
215
+ it { expect { subject }.to raise_error Raml::InvalidProperty, /body/ }
216
+ end
217
+ context 'when the body property is a map with non-map values' do
218
+ before { data['body'] = { 'text/xml' => 1 } }
219
+ it { expect { subject }.to raise_error Raml::InvalidProperty, /body/ }
220
+ end
221
+ end
222
+
223
+ context 'when a responses property is given' do
224
+ context 'when the responses property is well formed' do
225
+ let(:data) {
226
+ YAML.load(
227
+ %q(
228
+ responses:
229
+ 200:
230
+ body:
231
+ application/json:
232
+ example: !include examples/instagram-v1-media-popular-example.json
233
+ 503:
234
+ description: The service is currently unavailable.
235
+ )
236
+ )
237
+ }
238
+
239
+ it { expect { subject }.to_not raise_error }
240
+ it 'stores all as Raml::Response instances' do
241
+ expect( subject.responses.values ).to all( be_a Raml::Response )
242
+ subject.responses.keys.should contain_exactly(200, 503)
243
+ end
244
+ end
245
+ context 'when the responses property is not a map' do
246
+ before { data['responses'] = 1 }
247
+ it { expect { subject }.to raise_error Raml::InvalidProperty, /responses/ }
248
+ end
249
+ context 'when the responses property is not a map with string integer keys' do
250
+ before { data['responses'] = { '200' => {}} }
251
+ it { expect { subject }.not_to raise_error }
252
+ end
253
+ context 'when the responses property is not a map with non-string keys' do
254
+ before { data['responses'] = { 200 => 'x'} }
255
+ it { expect { subject }.to raise_error Raml::InvalidProperty, /responses/ }
256
+ end
257
+ end
258
+
259
+ context 'when an is property is given' do
260
+ let(:root) {
261
+ Raml::Root.new 'title' => 'x', 'baseUri' => 'http://foo.com', 'traits' => [
262
+ { 'secured' => {} },
263
+ { 'paged' => {} },
264
+ { 'rateLimited' => {} }
265
+ ]
266
+ }
267
+ context 'when the property is valid' do
268
+ context 'when the property is an array of trait references' do
269
+ let(:data) { { 'is' => [ 'secured', 'paged' ] } }
270
+ it { expect { subject }.to_not raise_error }
271
+ it 'should store the trait references' do
272
+ subject.traits.should all( be_a Raml::TraitReference )
273
+ subject.traits.map(&:name).should contain_exactly('secured', 'paged')
274
+ end
275
+ end
276
+ context 'when the property is an array of trait references with parameters' do
277
+ let(:data) { {
278
+ 'is' => [
279
+ {'secured' => {'tokenName' => 'access_token'}},
280
+ {'paged' => {'maxPages' => 10 }}
281
+ ]
282
+ } }
283
+ it { expect { subject }.to_not raise_error }
284
+ it 'should store the trait references' do
285
+ subject.traits.should all( be_a Raml::TraitReference )
286
+ subject.traits.map(&:name).should contain_exactly('secured', 'paged')
287
+ end
288
+ end
289
+ context 'when the property is an array of trait definitions' do
290
+ let(:data) { {
291
+ 'is' => [
292
+ {'queryParameters' => {'tokenName' => {'description'=>'foo'}}},
293
+ {'queryParameters' => {'numPages' => {'description'=>'bar'}}}
294
+ ]
295
+ } }
296
+ it { expect { subject }.to_not raise_error }
297
+ it 'should store the traits' do
298
+ subject.traits.should all( be_a Raml::Trait )
299
+ subject.traits[0].value.should eq ({'queryParameters' => {'tokenName' => {'description'=>'foo'}}})
300
+ subject.traits[1].value.should eq ({'queryParameters' => {'numPages' => {'description'=>'bar'}}})
301
+ end
302
+ end
303
+ context 'when the property is an array of mixed trait refrences, trait refrences with parameters, and trait definitions' do
304
+ let(:data) { {
305
+ 'is' => [
306
+ {'secured' => {'tokenName' => 'access_token'}},
307
+ {'queryParameters' => {'numPages' => {'description'=>'bar'}}},
308
+ 'rateLimited'
309
+ ]
310
+ } }
311
+ it { expect { subject }.to_not raise_error }
312
+ it 'should store the traits' do
313
+ subject.traits.select {|t| t.is_a? Raml::TraitReference }.map(&:name).should contain_exactly('secured', 'rateLimited')
314
+ subject.traits.select {|t| t.is_a? Raml::Trait}[0].value.should eq ({'queryParameters' => {'numPages' => {'description'=>'bar'}}})
315
+ end
316
+ end
317
+ end
318
+ context 'when the property is invalid' do
319
+ context 'when the property is not an array' do
320
+ let(:data) { { 'is' => 1 } }
321
+ it { expect { subject }.to raise_error Raml::InvalidProperty, /is/ }
322
+ end
323
+ context 'when the property is an array with elements other than a string or map' do
324
+ let(:data) { { 'is' => [1] } }
325
+ it { expect { subject }.to raise_error Raml::InvalidProperty, /is/ }
326
+ end
327
+ context 'when the property is an array an element that appears to be a trait name with parameters, but the params are not a map' do
328
+ let(:data) { { 'is' => [ { 'secured' => 1 } ] } }
329
+ it { expect { subject }.to raise_error Raml::InvalidProperty, /is/ }
330
+ end
331
+ end
332
+ end
333
+
334
+ context 'when the syntax tree contains optional properties' do
335
+ let(:data) {
336
+ YAML.load(%q(
337
+ description: Get a list of users
338
+ queryParameters:
339
+ page?:
340
+ description: Specify the page that you want to retrieve
341
+ type: integer
342
+ required: true
343
+ example: 1
344
+ protocols: [ HTTP, HTTPS ]
345
+ responses:
346
+ 200:
347
+ description: |
348
+ The list of popular media.
349
+ ))
350
+ }
351
+ it { expect {subject }.to raise_error Raml::InvalidProperty, /Optional properties/ }
352
+ end
353
+ end
354
+
355
+ describe '#merge' do
356
+ let(:method) { Raml::Method.new 'get' , method_data, root }
357
+ let(:trait ) { Raml::Trait.new( 'trait_name', trait_data , root).instantiate({}) }
358
+ context 'when the trait has a property set' do
359
+ context 'when the method does not have that property set' do
360
+ let(:method_data) { {} }
361
+ context 'description property' do
362
+ let(:trait_data) { {description: 'trait description'} }
363
+ it 'sets the property in the method' do
364
+ method.merge(trait).description.should eq trait.description
365
+ end
366
+ end
367
+ context 'protocols property' do
368
+ let(:trait_data) { {protocols: ['HTTPS']} }
369
+ it 'sets the property in the method' do
370
+ method.merge(trait).protocols.should eq trait.protocols
371
+ end
372
+ end
373
+ context 'headers properties' do
374
+ let(:trait_data) { {
375
+ 'headers' => {
376
+ 'header1' => {'description' => 'foo'},
377
+ 'header2' => {'description' => 'bar'}
378
+ }
379
+ } }
380
+ it 'adds the headers to the method' do
381
+ method.merge(trait).headers.keys.should contain_exactly('header1', 'header2')
382
+ end
383
+ end
384
+ context 'queryParameters properties' do
385
+ let(:trait_data) { {
386
+ 'queryParameters' => {
387
+ 'param1' => {'description' => 'foo'},
388
+ 'param2' => {'description' => 'bar'}
389
+ }
390
+ } }
391
+ it 'adds the headers to the method' do
392
+ method.merge(trait).query_parameters.keys.should contain_exactly('param1', 'param2')
393
+ end
394
+ end
395
+ context 'body property' do
396
+ let(:trait_data) { {
397
+ 'body' => {
398
+ 'text/mime1' => {'schema' => 'foo'},
399
+ 'text/mime2' => {'schema' => 'bar'}
400
+ }
401
+ } }
402
+ it 'adds the body media types to the method' do
403
+ method.merge(trait).bodies.keys.should contain_exactly('text/mime1', 'text/mime2')
404
+ end
405
+ end
406
+ context 'responses property' do
407
+ let(:trait_data) { {
408
+ 'responses' => {
409
+ 200 => {'description' => 'foo'},
410
+ 404 => {'description' => 'bar'}
411
+ }
412
+ } }
413
+ it 'adds the responses to the method' do
414
+ method.merge(trait).responses.keys.should contain_exactly(200, 404)
415
+ end
416
+ end
417
+ context 'usage property' do
418
+ let(:trait_data) { { 'usage' => 'trait usage' } }
419
+ it 'does not add the usage property to the method' do
420
+ method.merge(trait)
421
+ expect { method.usage }.to raise_error NoMethodError
422
+ end
423
+ end
424
+ end
425
+ context 'when the method has that property set' do
426
+ context 'description property' do
427
+ let(:method_data) { {description: 'method description'} }
428
+ let(:trait_data ) { {description: 'trait description' } }
429
+ it 'overwrites the method property' do
430
+ method.merge(trait).description.should eq 'trait description'
431
+ end
432
+ end
433
+ context 'protocols property' do
434
+ let(:method_data) { {protocols: ['HTTP' ]} }
435
+ let(:trait_data ) { {protocols: ['HTTPS']} }
436
+ it 'overwrites the method property' do
437
+ method.merge(trait).protocols.should eq ['HTTPS']
438
+ end
439
+ end
440
+ context 'headers properties' do
441
+ let(:method_data) { {
442
+ 'headers' => {
443
+ 'header1' => {'description' => 'foo'},
444
+ 'header2' => {'description' => 'bar'}
445
+ }
446
+ } }
447
+ context 'when the trait headers are different from the method headers' do
448
+ let(:trait_data) { {
449
+ 'headers' => {
450
+ 'header3' => {'description' => 'foo2'},
451
+ 'header4' => {'description' => 'bar2'}
452
+ }
453
+ } }
454
+ it 'adds the headers to the method' do
455
+ method.merge(trait).headers.keys.should contain_exactly('header1', 'header2', 'header3', 'header4')
456
+ end
457
+ end
458
+ context 'when the trait headers overlap the the method headers' do
459
+ let(:trait_data) { {
460
+ 'headers' => {
461
+ 'header2' => {'displayName' => 'Header 3'},
462
+ 'header3' => {'description' => 'foo2'},
463
+ 'header4' => {'description' => 'bar2'}
464
+ }
465
+ } }
466
+ it 'merges the matching headers and adds the non-matching headers to the method' do
467
+ method.merge(trait).headers.keys.should contain_exactly('header1', 'header2', 'header3', 'header4')
468
+ method.headers['header2'].display_name.should eq trait.headers['header2'].display_name
469
+ end
470
+ end
471
+ end
472
+ context 'queryParameters properties' do
473
+ let(:method_data) { {
474
+ 'queryParameters' => {
475
+ 'param1' => {'description' => 'foo'},
476
+ 'param2' => {'description' => 'bar'}
477
+ }
478
+ } }
479
+ context 'when the trait query parameters are different from the method headers' do
480
+ let(:trait_data) { {
481
+ 'queryParameters' => {
482
+ 'param3' => {'description' => 'foo2'},
483
+ 'param4' => {'description' => 'bar2'}
484
+ }
485
+ } }
486
+ it 'adds the query parameters to the method' do
487
+ method.merge(trait).query_parameters.keys.should contain_exactly('param1', 'param2', 'param3', 'param4')
488
+ end
489
+ end
490
+ context 'when the trait query parameters overlap the the method query parameters' do
491
+ let(:trait_data) { {
492
+ 'queryParameters' => {
493
+ 'param2' => {'displayName' => 'Param 3'},
494
+ 'param3' => {'description' => 'foo2'},
495
+ 'param4' => {'description' => 'bar2'}
496
+ }
497
+ } }
498
+ it 'merges the matching headers and adds the non-matching headers to the method' do
499
+ method.merge(trait).query_parameters.keys.should contain_exactly('param1', 'param2', 'param3', 'param4')
500
+ method.query_parameters['param2'].display_name.should eq trait.query_parameters['param2'].display_name
501
+ end
502
+ end
503
+ end
504
+ context 'body property' do
505
+ let(:method_data) { {
506
+ 'body' => {
507
+ 'text/mime1' => {'schema' => 'foo'},
508
+ 'text/mime2' => {'schema' => 'bar'}
509
+ }
510
+ } }
511
+ context 'when the trait query parameters are different from the method headers' do
512
+ let(:trait_data) { {
513
+ 'body' => {
514
+ 'text/mime3' => {'schema' => 'foo2'},
515
+ 'text/mime4' => {'schema' => 'bar2'}
516
+ }
517
+ } }
518
+ it 'adds the body media types to the method' do
519
+ method.merge(trait).bodies.keys.should contain_exactly('text/mime1', 'text/mime2', 'text/mime3', 'text/mime4')
520
+ end
521
+ end
522
+ context 'when the trait query parameters overlap the the method query parameters' do
523
+ let(:trait_data) { {
524
+ 'body' => {
525
+ 'text/mime2' => {'example' => 'Example 2'},
526
+ 'text/mime3' => {'schema' => 'foo2'},
527
+ 'text/mime4' => {'schema' => 'bar2'}
528
+ }
529
+ } }
530
+ it 'merges the matching media types and adds the non-matching media types to the method' do
531
+ method.merge(trait).bodies.keys.should contain_exactly('text/mime1', 'text/mime2', 'text/mime3', 'text/mime4')
532
+ method.bodies['text/mime2'].example.should eq trait.bodies['text/mime2'].example
533
+ end
534
+ end
535
+ end
536
+ context 'responses property' do
537
+ let(:method_data) { {
538
+ 'responses' => {
539
+ 200 => {'description' => 'foo', 'body' => { 'text/mime1' => {'schema' => 'schema1'} } },
540
+ 404 => {'description' => 'bar'}
541
+ }
542
+ } }
543
+ context 'when the trait response status codes are different from the method responses status codes' do
544
+ let(:trait_data) { {
545
+ 'responses' => {
546
+ 201 => {'description' => 'foo2'},
547
+ 403 => {'description' => 'bar2'}
548
+ }
549
+ } }
550
+ it 'adds the body media types to the method' do
551
+ method.merge(trait).responses.keys.should contain_exactly(200, 404, 201, 403)
552
+ end
553
+ end
554
+ context 'when the trait query parameters overlap the the method query parameters' do
555
+ let(:trait_data) { {
556
+ 'responses' => {
557
+ 200 => {'description' => 'foo', 'body' => { 'text/mime2' => {'schema' => 'schema2'} } },
558
+ 201 => {'description' => 'foo2'},
559
+ 403 => {'description' => 'bar2'}
560
+ }
561
+ } }
562
+ it 'merges the matching media types and adds the non-matching media types to the method' do
563
+ method.merge(trait).responses.keys.should contain_exactly(200, 404, 201, 403)
564
+ method.responses[200].bodies.keys.should contain_exactly('text/mime1', 'text/mime2')
565
+ end
566
+ end
567
+ end
568
+ end
569
+ end
570
+ end
571
+
572
+ describe '#apply_traits' do
573
+ let(:root_data) { {
574
+ 'title' => 'x',
575
+ 'baseUri' => 'http://foo.com',
576
+ '/users' => {
577
+ '/comments' => {
578
+ 'is' => resource_trait_data,
579
+ 'get' => method_data
580
+ }
581
+ }
582
+ } }
583
+ let(:method) { root.resources['/users'].resources['/comments'].methods['get'] }
584
+ before { method.apply_traits }
585
+ describe 'order application' do
586
+ context 'when given no resource traits' do
587
+ let(:resource_trait_data) { [] }
588
+ context 'when method has a trait' do
589
+ let(:method_data) { { 'is' => [ { 'description' => 'trait description' } ] } }
590
+ it 'applies the method trait' do
591
+ method.description.should eq 'trait description'
592
+ end
593
+ end
594
+ context 'when the method has multiple traits' do
595
+ let(:method_data) { {
596
+ 'is' => [
597
+ {
598
+ 'description' => 'trait description',
599
+ 'headers' => { 'header1' => { 'description' => 'header1' } }
600
+ },
601
+ {
602
+ 'description' => 'trait description 2',
603
+ 'headers' => { 'header2' => { 'description' => 'header2' } }
604
+ }
605
+ ]
606
+ } }
607
+ it 'applies them in order of precedence, right to left' do
608
+ method.description.should eq 'trait description 2'
609
+ method.headers.keys.should contain_exactly('header1', 'header2')
610
+ end
611
+ end
612
+ end
613
+ context 'when given resource traits' do
614
+ let(:resource_trait_data) { [ { 'description' => 'resource trait description' } ] }
615
+ context 'when the method has no traits' do
616
+ let(:method_data) { {} }
617
+ it 'applies the resource trait' do
618
+ method.description.should eq 'resource trait description'
619
+ end
620
+ end
621
+ context 'when the method has traits' do
622
+ let(:resource_trait_data) {
623
+ [
624
+ {
625
+ 'description' => 'trait4 description',
626
+ 'headers' => {
627
+ 'header3' => { 'description' => 'trait4' },
628
+ 'header4' => { 'description' => 'trait4' }
629
+ }
630
+ },
631
+ {
632
+ 'description' => 'trait3 description',
633
+ 'headers' => {
634
+ 'header2' => { 'description' => 'trait3' },
635
+ 'header3' => { 'description' => 'trait3' }
636
+ }
637
+ }
638
+ ]
639
+ }
640
+ let(:method_data) { {
641
+ 'description' => 'method description',
642
+ 'is' => [
643
+ {
644
+ 'description' => 'trait2 description',
645
+ 'headers' => {
646
+ 'header1' => { 'description' => 'trait2' },
647
+ 'header2' => { 'description' => 'trait2' }
648
+ }
649
+ },
650
+ {
651
+ 'description' => 'trait1 description',
652
+ 'headers' => {
653
+ 'header1' => { 'description' => 'trait1' }
654
+ }
655
+ }
656
+ ]
657
+ } }
658
+ it 'applies method traits first in reverse order, then resource traits in reverse order' do
659
+ method.description.should eq 'method description'
660
+ method.headers.keys.should contain_exactly('header1','header2', 'header3', 'header4')
661
+ method.headers['header1'].description.should eq 'trait1'
662
+ method.headers['header2'].description.should eq 'trait2'
663
+ method.headers['header3'].description.should eq 'trait3'
664
+ method.headers['header4'].description.should eq 'trait4'
665
+ end
666
+ end
667
+ end
668
+ end
669
+ describe 'reserved parameters' do
670
+ let(:resource_trait_data) { [] }
671
+ context 'resourcePath' do
672
+ let(:method_data) { { 'is' => [ { 'description' => 'trait <<resourcePath>>' } ] } }
673
+ it 'instances traits with the reserved parameter' do
674
+ method.description.should eq 'trait /users/comments'
675
+ end
676
+ end
677
+ context 'resourcePathName' do
678
+ let(:method_data) { { 'is' => [ { 'description' => 'trait <<resourcePathName>>' } ] } }
679
+ it 'instances traits with the reserved parameter' do
680
+ method.description.should eq 'trait comments'
681
+ end
682
+ end
683
+ context 'methodName' do
684
+ let(:method_data) { { 'is' => [ { 'description' => 'trait <<methodName>>' } ] } }
685
+ it 'instances traits with the reserved parameter' do
686
+ method.description.should eq 'trait get'
687
+ end
688
+ end
689
+ end
690
+ end
691
+
692
+ describe '#document' do
693
+ it 'returns a String' do
694
+ subject.document.should be_a String
695
+ end
696
+ it 'should render the template' do
697
+ mock(Slim::Template).new(/templates\/method.slim\z/, is_a(Hash)).mock!.render(is_a(Raml::Node)) { '' }
698
+ subject.document
699
+ end
700
+ end
701
+ end