fdoc 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. data/Gemfile +2 -0
  2. data/README.md +120 -0
  3. data/Rakefile +7 -0
  4. data/bin/fdoc_to_html +77 -0
  5. data/fdoc.gemspec +36 -0
  6. data/lib/endpoint-schema.yaml +30 -0
  7. data/lib/fdoc.rb +46 -0
  8. data/lib/fdoc/endpoint.rb +110 -0
  9. data/lib/fdoc/endpoint_scaffold.rb +132 -0
  10. data/lib/fdoc/meta_service.rb +46 -0
  11. data/lib/fdoc/presenters/endpoint_presenter.rb +152 -0
  12. data/lib/fdoc/presenters/html_presenter.rb +58 -0
  13. data/lib/fdoc/presenters/meta_service_presenter.rb +65 -0
  14. data/lib/fdoc/presenters/response_code_presenter.rb +32 -0
  15. data/lib/fdoc/presenters/schema_presenter.rb +138 -0
  16. data/lib/fdoc/presenters/service_presenter.rb +56 -0
  17. data/lib/fdoc/service.rb +88 -0
  18. data/lib/fdoc/spec_watcher.rb +48 -0
  19. data/lib/fdoc/templates/endpoint.html.erb +75 -0
  20. data/lib/fdoc/templates/meta_service.html.erb +60 -0
  21. data/lib/fdoc/templates/service.html.erb +54 -0
  22. data/lib/fdoc/templates/styles.css +63 -0
  23. data/spec/fdoc/endpoint_scaffold_spec.rb +242 -0
  24. data/spec/fdoc/endpoint_spec.rb +243 -0
  25. data/spec/fdoc/presenters/endpoint_presenter_spec.rb +93 -0
  26. data/spec/fdoc/presenters/service_presenter_spec.rb +18 -0
  27. data/spec/fdoc/service_spec.rb +63 -0
  28. data/spec/fixtures/members/add-PUT.fdoc +20 -0
  29. data/spec/fixtures/members/draft-POST.fdoc +5 -0
  30. data/spec/fixtures/members/list/GET.fdoc +50 -0
  31. data/spec/fixtures/members/list/complex-params-GET.fdoc +94 -0
  32. data/spec/fixtures/members/list/filter-GET.fdoc +60 -0
  33. data/spec/fixtures/members/members.fdoc.service +11 -0
  34. data/spec/fixtures/sample_group.fdoc.meta +9 -0
  35. data/spec/spec_helper.rb +2 -0
  36. metadata +174 -0
@@ -0,0 +1,242 @@
1
+ require 'spec_helper'
2
+
3
+ describe Fdoc::EndpointScaffold do
4
+ subject { described_class.new('spec/fixtures/network-GET.fdoc') }
5
+ let(:action_parameters) { {
6
+ "scaffold" => true,
7
+ "description" => "???",
8
+ "responseCodes" => []
9
+ } }
10
+
11
+ describe "#consume_request" do
12
+ request_params = {
13
+ "depth" => 5,
14
+ "max_connections" => 20,
15
+ "root_node" => "41EAF42"
16
+ }
17
+
18
+ before(:each) do
19
+ subject.request_parameters.should be_empty
20
+ end
21
+
22
+ it "creates properties for top-level keys, and populates them with examples" do
23
+ subject.consume_request(request_params, true)
24
+ subject.request_parameters["type"].should == nil
25
+ subject.request_parameters["properties"].should have(3).keys
26
+ subject.request_parameters["properties"]["depth"]["type"].should == "integer"
27
+ subject.request_parameters["properties"]["max_connections"]["example"].should == 20
28
+ subject.request_parameters["properties"]["root_node"]["type"].should == "string"
29
+ end
30
+
31
+ it "infers boolean types" do
32
+ bool_params = {
33
+ "with_cheese" => false,
34
+ "hold_the_lettuce" => true
35
+ }
36
+ subject.consume_request(bool_params)
37
+ subject.request_parameters["properties"].should have(2).keys
38
+ subject.request_parameters["properties"]["with_cheese"]["type"].should == "boolean"
39
+ subject.request_parameters["properties"]["hold_the_lettuce"]["type"].should == "boolean"
40
+ end
41
+
42
+ context "infers formats" do
43
+ it "detects date-time formats as objects, or as is08601 strings" do
44
+ datetime_params = {
45
+ "time_str" => Time.now.iso8601,
46
+ "time_obj" => Time.now
47
+ }
48
+ subject.consume_request(datetime_params)
49
+ subject.request_parameters["properties"].should have(2).keys
50
+ subject.request_parameters["properties"]["time_str"]["type"].should == "string"
51
+ subject.request_parameters["properties"]["time_str"]["format"].should == "date-time"
52
+ subject.request_parameters["properties"]["time_obj"]["type"].should == "string"
53
+ subject.request_parameters["properties"]["time_obj"]["format"].should == "date-time"
54
+ end
55
+
56
+ it "detects uri formats" do
57
+ uri_params = {
58
+ "sample_uri" => "http://my.example.com"
59
+ }
60
+ subject.consume_request(uri_params)
61
+ subject.request_parameters["properties"].should have(1).keys
62
+ subject.request_parameters["properties"]["sample_uri"]["type"].should == "string"
63
+ subject.request_parameters["properties"]["sample_uri"]["format"].should == "uri"
64
+ end
65
+
66
+ it "detects color formats (hex only for now)" do
67
+ color_params = { "page_color" => "#AABBCC" }
68
+ subject.consume_request(color_params)
69
+ subject.request_parameters["properties"]["page_color"]["type"].should == "string"
70
+ subject.request_parameters["properties"]["page_color"]["format"].should == "color"
71
+ end
72
+ end
73
+
74
+ it "uses strings (not symbols) as keys" do
75
+ mixed_params = {
76
+ :with_symbol => false,
77
+ "with_string" => true
78
+ }
79
+ subject.consume_request(mixed_params)
80
+ subject.request_parameters["properties"].should have(2).keys
81
+ subject.request_parameters["properties"].should have_key "with_symbol"
82
+ subject.request_parameters["properties"].should_not have_key :with_symbol
83
+ subject.request_parameters["properties"].should have_key "with_string"
84
+ subject.request_parameters["properties"].should_not have_key :with_string
85
+ end
86
+
87
+ it "uses strings (not symbols) for keys of nested hashes" do
88
+ mixed_params = {
89
+ "nested_object" => {
90
+ :with_symbol => false,
91
+ "with_string" => true
92
+ }
93
+ }
94
+
95
+ subject.consume_request(mixed_params)
96
+ subject.request_parameters["properties"]["nested_object"]["properties"].keys.sort.should == ["with_string", "with_symbol"]
97
+ end
98
+
99
+ it "uses strings (not symbols) for nested hashes inside arrays" do
100
+ mixed_params = {
101
+ "nested_array" => [
102
+ {
103
+ :with_symbol => false,
104
+ "with_string" => true
105
+ }
106
+ ]
107
+ }
108
+
109
+ subject.consume_request(mixed_params)
110
+ subject.request_parameters["properties"]["nested_array"]["items"]["properties"].keys.sort.should == ["with_string", "with_symbol"]
111
+ end
112
+
113
+ it "produces a valid JSON schema for the response" do
114
+ subject.consume_request(request_params)
115
+ subject.request_parameters["properties"].should have(3).keys
116
+ JSON::Validator.validate!(subject.request_parameters, request_params).should be_true
117
+ end
118
+ end
119
+
120
+ describe "#consume_response" do
121
+ let(:response_params) { {
122
+ "nodes" => [{
123
+ "id" => "12941",
124
+ "name" => "Bobjoe Smith",
125
+ "linked_to" => [ "111", "121", "999"]
126
+ }, {
127
+ "id" => "111",
128
+ "name" => "Sally",
129
+ "linked_to" => ["12941"]
130
+ }, {
131
+ "id" => "121",
132
+ "name" => "Captain Smellypants",
133
+ "linked_to" => ["12941", "999"]
134
+ }, {
135
+ "id" => "999",
136
+ "name" => "Linky McLinkface",
137
+ "linked_to" => ["12941", "121"]
138
+ }
139
+ ],
140
+ "root_node" => {
141
+ "id" => "12941",
142
+ "name" => "Bobjoe Smith",
143
+ "linked_to" => [ "111", "121", "999"]
144
+ },
145
+ "version" => 1,
146
+ "std_dev" => 1.231,
147
+ "updated_at" => nil
148
+ } }
149
+
150
+
151
+
152
+ context "for succesful responses" do
153
+ before(:each) do
154
+ subject.should have(0).response_codes
155
+ end
156
+
157
+ it "adds response codes" do
158
+ subject.consume_response({}, "200 OK")
159
+ subject.should have(1).response_codes
160
+
161
+ subject.consume_response({}, "201 Created")
162
+ subject.should have(2).response_codes
163
+ end
164
+
165
+ it "does not add duplicate response codes" do
166
+ subject.consume_response({}, "200 OK")
167
+ subject.should have(1).response_codes
168
+
169
+ subject.consume_response({}, "200 OK")
170
+ subject.should have(1).response_codes
171
+
172
+ subject.response_codes.each do |response|
173
+ response["description"].should == "???"
174
+ end
175
+ end
176
+
177
+ it "creates properties for top-level keys, and populates them with examples" do
178
+ subject.consume_response(response_params, "200 OK")
179
+ subject.response_parameters["type"].should == nil
180
+ subject.response_parameters["properties"].keys.should =~ ["nodes", "root_node", "std_dev", "version", "updated_at"]
181
+
182
+ subject.response_parameters["properties"]["nodes"]["type"].should == "array"
183
+ subject.response_parameters["properties"]["nodes"]["description"].should == "???"
184
+ subject.response_parameters["properties"]["nodes"]["required"].should == "???"
185
+
186
+ subject.response_parameters["properties"]["root_node"]["type"].should == "object"
187
+ subject.response_parameters["properties"]["root_node"]["description"].should == "???"
188
+ subject.response_parameters["properties"]["root_node"]["required"].should == "???"
189
+
190
+ subject.response_parameters["properties"]["version"]["type"].should == "integer"
191
+ subject.response_parameters["properties"]["std_dev"]["type"].should == "number"
192
+ end
193
+
194
+ it "populates items in arrays" do
195
+ subject.consume_response(response_params, "200 OK")
196
+ subject.response_parameters["properties"]["nodes"]["type"].should == "array"
197
+ subject.response_parameters["properties"]["nodes"]["items"]["type"].should == "object"
198
+ subject.response_parameters["properties"]["nodes"]["items"]["properties"].keys.sort.should == [
199
+ "id", "linked_to","name"]
200
+ end
201
+
202
+ it "turns nil into null" do
203
+ subject.consume_response(response_params, "200 OK")
204
+ subject.response_parameters["properties"]["updated_at"]["type"].should == "null"
205
+ end
206
+
207
+ it "uses strings (not symbols) as keys" do
208
+ mixed_params = {
209
+ :with_symbol => false,
210
+ "with_string" => true
211
+ }
212
+ subject.consume_response(mixed_params, "200 OK")
213
+ subject.response_parameters["properties"].should have(2).keys
214
+ subject.response_parameters["properties"].should have_key "with_symbol"
215
+ subject.response_parameters["properties"].should_not have_key :with_symbol
216
+ subject.response_parameters["properties"].should have_key "with_string"
217
+ subject.response_parameters["properties"].should_not have_key :with_string
218
+ end
219
+
220
+ it "produces a valid JSON schema for the response" do
221
+ subject.consume_response(response_params, "200 OK")
222
+ JSON::Validator.validate!(subject.response_parameters, response_params).should be_true
223
+ end
224
+ end
225
+
226
+ context "for unsuccessful responses" do
227
+ it "adds response codes" do
228
+ subject.should have(0).response_codes
229
+ subject.consume_response({}, "400 Bad Request", false)
230
+ subject.should have(1).response_codes
231
+ subject.consume_response({}, "404 Not Found", false)
232
+ subject.should have(2).response_codes
233
+ end
234
+
235
+ it "does not modify the response_parameters" do
236
+ subject.response_parameters.should be_empty
237
+ subject.consume_response(response_params, "403 Forbidden", false)
238
+ subject.response_parameters.should be_empty
239
+ end
240
+ end
241
+ end
242
+ end
@@ -0,0 +1,243 @@
1
+ require 'spec_helper'
2
+
3
+ describe Fdoc::Endpoint do
4
+ let(:endpoint) { test_service.open(*fdoc_fixture) }
5
+ let(:fdoc_fixture) { ["GET", "members/list"] }
6
+ let (:test_service) { Fdoc::Service.new('spec/fixtures') }
7
+ subject { endpoint }
8
+
9
+ def remove_optional(obj)
10
+ case obj
11
+ when Hash
12
+ res = {}
13
+ obj.each do |k, v|
14
+ next if k =~ /optional/
15
+ res[k] = remove_optional(v)
16
+ end
17
+ obj.clear
18
+ obj.merge!(res)
19
+ when Array then obj.map { |v| remove_optional(v) }
20
+ else obj
21
+ end
22
+ end
23
+
24
+ describe "#verb" do
25
+ it "infers the verb from the filename and service" do
26
+ subject.verb.should == "GET"
27
+ end
28
+ end
29
+
30
+ describe "#path" do
31
+ it "infers its path from the filename and service" do
32
+ subject.path.should == "members/list"
33
+ end
34
+ end
35
+
36
+ describe "#consume_request" do
37
+ subject { endpoint.consume_request(params) }
38
+ let(:params) {
39
+ {
40
+ "limit" => 0,
41
+ "offset" => 100,
42
+ "order_by" => "name"
43
+ }
44
+ }
45
+
46
+ context "with a well-behaved request" do
47
+ it "returns true" do
48
+ subject.should be_true
49
+ end
50
+ end
51
+
52
+ context "when the response contains additional properties" do
53
+ before { params.merge!("extra_goodness" => true) }
54
+
55
+ it "should have the unknown keys in the error message" do
56
+ expect { subject }.to raise_exception(JSON::Schema::ValidationError, /extra_goodness/)
57
+ end
58
+ end
59
+
60
+ context "when the response contains an unknown enum value" do
61
+ before { params.merge!("order_by" => "some_stuff") }
62
+
63
+ it "should have the value in the error messages" do
64
+ expect { subject }.to raise_exception(JSON::Schema::ValidationError, /some_stuff/)
65
+ end
66
+ end
67
+
68
+ context "when the response encounters an object of an known type" do
69
+ before { params.merge!("offset" => "woot") }
70
+
71
+ it "should have the Ruby type in the error message" do
72
+ expect { subject }.to raise_exception(JSON::Schema::ValidationError, /String/)
73
+ end
74
+ end
75
+
76
+ context "complex examples" do
77
+ let(:fdoc_fixture) { ["GET", "/members/list/complex-params"] }
78
+ let(:params) {
79
+ {
80
+ "toplevel_param" => "here",
81
+ "optional_nested_array" => [
82
+ {
83
+ "required_param" => "here",
84
+ "optional_param" => "here"
85
+ }
86
+ ],
87
+ "required_nested_array" => [
88
+ {
89
+ "required_param" => "here",
90
+ "optional_param" => "here",
91
+ "optional_second_nested_object" => {
92
+ "required_param" => "here",
93
+ "optional_param" => "here"
94
+ }
95
+ },
96
+ ],
97
+ "optional_nested_object" => {
98
+ "required_param" => "here",
99
+ "optional_param" => "here"
100
+ },
101
+ "required_nested_object" => {
102
+ "required_param" => "here",
103
+ "optional_param" => "here",
104
+ "optional_second_nested_object" => {
105
+ "required_param" => "here",
106
+ "optional_param" => "here"
107
+ }
108
+ },
109
+ }
110
+ }
111
+
112
+ it "is successful" do
113
+ subject.should be_true
114
+ end
115
+
116
+ context "with no optional keys" do
117
+ before { remove_optional(params) }
118
+
119
+ it "does not contain optional keys" do
120
+ params.keys.sort.should == ["required_nested_array", "required_nested_object", "toplevel_param"]
121
+ end
122
+
123
+ it "is successful" do
124
+ subject.should be_true
125
+ end
126
+ end
127
+
128
+ context "non documented field added" do
129
+ before { params.merge!("non_documented" => true) }
130
+ it "raises an error" do
131
+ expect { subject }.to raise_exception(JSON::Schema::ValidationError, /non_documented/)
132
+ end
133
+ end
134
+
135
+ context "non document field in an optional array" do
136
+ before { params["optional_nested_array"][0].merge!("non_documented" => true) }
137
+
138
+ it "raises an error" do
139
+ expect { subject }.to raise_exception(JSON::Schema::ValidationError, /non_documented/)
140
+ end
141
+ end
142
+
143
+ context "non document field in a required array" do
144
+ before { params["required_nested_array"][0].merge!("non_documented" => true) }
145
+
146
+ it "raises an error" do
147
+ expect { subject }.to raise_exception(JSON::Schema::ValidationError, /non_documented/)
148
+ end
149
+ end
150
+
151
+ context "non document field in an optional object" do
152
+ before { params["optional_nested_object"].merge!("non_documented" => true) }
153
+
154
+ it "raises an error" do
155
+ expect { subject }.to raise_exception(JSON::Schema::ValidationError, /non_documented/)
156
+ end
157
+ end
158
+
159
+ context "non document field in a required object" do
160
+ before { params["required_nested_object"].merge!("non_documented" => true) }
161
+
162
+ it "raises an error" do
163
+ expect { subject }.to raise_exception(JSON::Schema::ValidationError, /non_documented/)
164
+ end
165
+ end
166
+
167
+ context "non document field in a deeply nested object" do
168
+ before { params["required_nested_object"]["optional_second_nested_object"].merge!("non_documented" => true) }
169
+
170
+ it "raises an error" do
171
+ expect { subject }.to raise_exception(JSON::Schema::ValidationError, /non_documented/)
172
+ end
173
+ end
174
+
175
+ context "required field in a deeply nested object is missing" do
176
+ before { params["required_nested_object"]["optional_second_nested_object"].delete("required_param") }
177
+
178
+ it "raises an error" do
179
+ expect { subject }.to raise_exception(JSON::Schema::ValidationError, /required_param/)
180
+ end
181
+ end
182
+
183
+ context "non document field in a deeply nested object in an array" do
184
+ before { params["required_nested_array"][0]["optional_second_nested_object"].merge!("non_documented" => true) }
185
+
186
+ it "raises an error" do
187
+ expect { subject }.to raise_exception(JSON::Schema::ValidationError, /non_documented/)
188
+ end
189
+ end
190
+
191
+ context "required field in a deeply nested object is missing" do
192
+ before { params["required_nested_array"][0]["optional_second_nested_object"].delete("required_param") }
193
+
194
+ it "raises an error" do
195
+ expect { subject }.to raise_exception(JSON::Schema::ValidationError, /required_param/)
196
+ end
197
+ end
198
+ end
199
+ end
200
+
201
+ describe "#consume_response" do
202
+ good_response_params = {
203
+ "members" => [
204
+ {"name" => "Captain Smelly Pants"},
205
+ {"name" => "Sally Pants"},
206
+ {"name" => "Joe Shorts"}
207
+ ]
208
+ }
209
+
210
+ it "throws an error when there is no response corresponding to the success-code error" do
211
+ expect { subject.consume_response(good_response_params, "404 Not Found") }.to raise_exception Fdoc::UndocumentedResponseCode
212
+ expect { subject.consume_response(good_response_params, "200 OK", false) }.to raise_exception Fdoc::UndocumentedResponseCode
213
+ end
214
+
215
+ context "for successful responses" do
216
+ it "validates the response parameters against the schema" do
217
+ subject.consume_response(good_response_params, "200 OK").should be_true
218
+ end
219
+
220
+ context "with unknown keys" do
221
+ it "throws an error when there an unknown key at the top level" do
222
+ bad_params = good_response_params.merge({"extra_goodness" => true})
223
+ expect { subject.consume_response(bad_params, "200 OK") }.to raise_exception JSON::Schema::ValidationError
224
+ end
225
+
226
+ it "throws an error when there is an unknown key a few layers deep" do
227
+ bad_nested_params = good_response_params.dup
228
+ bad_nested_params["members"][0]["smelliness"] = "the_max"
229
+ expect { subject.consume_response(bad_nested_params, "200 OK") }.to raise_exception JSON::Schema::ValidationError
230
+ end
231
+ end
232
+ end
233
+
234
+ context "for unsuccessful responses" do
235
+ context "when there is a valid success-code response" do
236
+ it "does not throw an error with bad response parameters" do
237
+ bad_params = good_response_params.merge({"extra_goodness" => true})
238
+ subject.consume_response(bad_params, "400 Bad Request", false).should be_true
239
+ end
240
+ end
241
+ end
242
+ end
243
+ end