fdoc 0.2.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 (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