webspicy 0.15.7 → 0.16.3

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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +71 -24
  3. data/bin/webspicy +30 -14
  4. data/examples/restful/Gemfile +2 -2
  5. data/examples/restful/Gemfile.lock +53 -33
  6. data/examples/restful/Rakefile +0 -1
  7. data/examples/restful/app.rb +4 -1
  8. data/examples/restful/webspicy/config.rb +8 -0
  9. data/examples/restful/webspicy/rack.rb +1 -1
  10. data/examples/restful/webspicy/real.rb +1 -1
  11. data/examples/restful/webspicy/schema.fio +2 -2
  12. data/examples/restful/webspicy/support/must_be_authenticated.rb +2 -2
  13. data/examples/restful/webspicy/support/todo_removed.rb +18 -0
  14. data/examples/restful/webspicy/todo/deleteTodo.yml +4 -1
  15. data/examples/restful/webspicy/todo/getTodoSingleServiceFormat.yml +46 -0
  16. data/examples/restful/webspicy/todo/options.yml +1 -1
  17. data/examples/restful/webspicy/todo/patchTodo.yml +3 -0
  18. data/examples/restful/webspicy/todo/postFile.yml +1 -1
  19. data/examples/single_spec/spec.yml +59 -0
  20. data/examples/website/config.rb +2 -0
  21. data/examples/website/schema.fio +1 -0
  22. data/examples/website/specification/get-http.yml +30 -0
  23. data/examples/website/specification/get-https.yml +30 -0
  24. data/lib/finitio/webspicy/scalars.fio +25 -0
  25. data/lib/webspicy.rb +48 -17
  26. data/lib/webspicy/checker.rb +2 -2
  27. data/lib/webspicy/configuration.rb +70 -14
  28. data/lib/webspicy/configuration/scope.rb +162 -0
  29. data/lib/webspicy/configuration/single_url.rb +58 -0
  30. data/lib/webspicy/configuration/single_yml_file.rb +30 -0
  31. data/lib/webspicy/formaldoc.fio +23 -8
  32. data/lib/webspicy/mocker.rb +8 -8
  33. data/lib/webspicy/mocker/config.ru +5 -0
  34. data/lib/webspicy/openapi.rb +1 -0
  35. data/lib/webspicy/openapi/generator.rb +127 -0
  36. data/lib/webspicy/{resource.rb → specification.rb} +28 -5
  37. data/lib/webspicy/specification/file_upload.rb +37 -0
  38. data/lib/webspicy/specification/postcondition.rb +16 -0
  39. data/lib/webspicy/specification/precondition.rb +19 -0
  40. data/lib/webspicy/specification/precondition/global_request_headers.rb +35 -0
  41. data/lib/webspicy/specification/precondition/robust_to_invalid_input.rb +68 -0
  42. data/lib/webspicy/{resource → specification}/service.rb +11 -6
  43. data/lib/webspicy/specification/test_case.rb +139 -0
  44. data/lib/webspicy/support.rb +1 -0
  45. data/lib/webspicy/support/colorize.rb +28 -0
  46. data/lib/webspicy/support/status_range.rb +6 -1
  47. data/lib/webspicy/tester.rb +16 -11
  48. data/lib/webspicy/tester/asserter.rb +3 -2
  49. data/lib/webspicy/tester/assertions.rb +5 -1
  50. data/lib/webspicy/tester/client.rb +63 -0
  51. data/lib/webspicy/tester/client/http_client.rb +154 -0
  52. data/lib/webspicy/tester/client/rack_test_client.rb +188 -0
  53. data/lib/webspicy/tester/client/support.rb +65 -0
  54. data/lib/webspicy/tester/invocation.rb +218 -0
  55. data/lib/webspicy/tester/rspec_asserter.rb +108 -0
  56. data/lib/webspicy/tester/rspec_matchers.rb +104 -0
  57. data/lib/webspicy/version.rb +2 -2
  58. data/spec/{unit/spec_helper.rb → spec_helper.rb} +0 -0
  59. data/spec/unit/configuration/scope/test_each_service.rb +49 -0
  60. data/spec/unit/configuration/scope/test_each_specification.rb +68 -0
  61. data/spec/unit/configuration/scope/test_expand_example.rb +65 -0
  62. data/spec/unit/configuration/scope/test_to_real_url.rb +82 -0
  63. data/spec/unit/openapi/test_generator.rb +28 -0
  64. data/spec/unit/specification/precondition/test_global_request_headers.rb +42 -0
  65. data/spec/unit/{resource → specification}/service/test_dress_params.rb +2 -2
  66. data/spec/unit/specification/test_case/test_mutate.rb +24 -0
  67. data/spec/unit/{resource → specification}/test_instantiate_url.rb +5 -5
  68. data/spec/unit/{resource → specification}/test_url_placeholders.rb +4 -4
  69. data/spec/unit/test_configuration.rb +24 -7
  70. data/spec/unit/tester/client/test_around.rb +61 -0
  71. data/spec/unit/tester/test_asserter.rb +51 -0
  72. data/spec/unit/tester/test_assertions.rb +4 -4
  73. data/tasks/test.rake +3 -1
  74. metadata +88 -45
  75. data/LICENSE.md +0 -22
  76. data/lib/webspicy/client.rb +0 -61
  77. data/lib/webspicy/client/http_client.rb +0 -145
  78. data/lib/webspicy/client/rack_test_client.rb +0 -181
  79. data/lib/webspicy/client/support.rb +0 -48
  80. data/lib/webspicy/file_upload.rb +0 -35
  81. data/lib/webspicy/postcondition.rb +0 -14
  82. data/lib/webspicy/precondition.rb +0 -15
  83. data/lib/webspicy/resource/service/invocation.rb +0 -212
  84. data/lib/webspicy/resource/service/test_case.rb +0 -132
  85. data/lib/webspicy/scope.rb +0 -160
  86. data/spec/unit/client/test_around.rb +0 -59
  87. data/spec/unit/scope/test_each_resource.rb +0 -66
  88. data/spec/unit/scope/test_each_service.rb +0 -47
  89. data/spec/unit/scope/test_expand_example.rb +0 -63
  90. data/spec/unit/scope/test_to_real_url.rb +0 -80
@@ -0,0 +1,162 @@
1
+ module Webspicy
2
+ class Configuration
3
+ class Scope
4
+
5
+ def initialize(config)
6
+ @config = config
7
+ end
8
+ attr_reader :config
9
+
10
+ def preconditions
11
+ config.preconditions
12
+ end
13
+
14
+ def postconditions
15
+ config.postconditions
16
+ end
17
+
18
+ ###
19
+ ### Eachers -- Allow navigating the web service definitions
20
+ ###
21
+
22
+ # Yields each specification file in the current scope
23
+ def each_specification_file(&bl)
24
+ return enum_for(:each_specification_file) unless block_given?
25
+ _each_specification_file(config, &bl)
26
+ end
27
+
28
+ # Recursive implementation of `each_specification_file` for each
29
+ # folder in the configuration.
30
+ def _each_specification_file(config)
31
+ folder = config.folder
32
+ folder.glob("**/*.yml").select(&to_filter_proc(config.file_filter)).each do |file|
33
+ yield file, folder
34
+ end
35
+ end
36
+ private :_each_specification_file
37
+
38
+ # Yields each specification in the current scope in turn.
39
+ def each_specification(&bl)
40
+ return enum_for(:each_specification) unless block_given?
41
+ each_specification_file do |file, folder|
42
+ yield Webspicy.specification(file.load, file, self)
43
+ end
44
+ end
45
+
46
+ def each_service(specification, &bl)
47
+ specification.services.select(&to_filter_proc(config.service_filter)).each(&bl)
48
+ end
49
+
50
+ def each_example(service, &bl)
51
+ service.examples
52
+ .map{|e| expand_example(service, e) }
53
+ .select(&to_filter_proc(config.test_case_filter))
54
+ .each(&bl) if config.run_examples?
55
+ end
56
+
57
+ def each_counterexamples(service, &bl)
58
+ service.counterexamples
59
+ .map{|e| expand_example(service, e) }
60
+ .select(&to_filter_proc(config.test_case_filter))
61
+ .each(&bl) if config.run_counterexamples?
62
+ end
63
+
64
+ def each_generated_counterexamples(service, &bl)
65
+ Webspicy.with_scope(self) do
66
+ service.generated_counterexamples
67
+ .map{|e| expand_example(service, e) }
68
+ .select(&to_filter_proc(config.test_case_filter))
69
+ .each(&bl) if config.run_generated_counterexamples?
70
+ end if config.run_generated_counterexamples?
71
+ end
72
+
73
+ def each_testcase(service, &bl)
74
+ each_example(service, &bl)
75
+ each_counterexamples(service, &bl)
76
+ each_generated_counterexamples(service, &bl)
77
+ end
78
+
79
+ ###
80
+ ### Schemas -- For parsing input and output data schemas found in
81
+ ### web service definitions
82
+ ###
83
+
84
+ # Parses a Finitio schema based on the data system.
85
+ def parse_schema(fio)
86
+ data_system.parse(fio)
87
+ end
88
+
89
+ # Returns the Data system to use for parsing schemas
90
+ def data_system
91
+ @data_system ||= config.data_system
92
+ end
93
+
94
+
95
+ ###
96
+ ### Service invocation: abstract the configuration about what client is
97
+ ### used and how to instantiate it
98
+ ###
99
+
100
+ # Returns an instance of the client to use to invoke web services
101
+ def get_client
102
+ config.client.new(self)
103
+ end
104
+
105
+ # Convert an instantiated URL found in a webservice definition
106
+ # to a real URL, using the configuration host.
107
+ #
108
+ # When no host resolved on the configuration and the url is not
109
+ # already an absolute URL, yields the block if given, or raise
110
+ # an exception.
111
+ def to_real_url(url, test_case = nil, &bl)
112
+ case config.host
113
+ when Proc
114
+ config.host.call(url, test_case)
115
+ when String
116
+ url =~ /^http/ ? url : "#{config.host}#{url}"
117
+ else
118
+ return url if url =~ /^http/
119
+ return yield(url) if block_given?
120
+ raise "Unable to resolve `#{url}` : no host resolver provided\nSee `Configuration#host="
121
+ end
122
+ end
123
+
124
+ ###
125
+ ### Private methods
126
+ ###
127
+
128
+ private
129
+
130
+ def expand_example(service, example)
131
+ return example unless service.default_example
132
+ h1 = service.default_example.to_info
133
+ h2 = example.to_info
134
+ ex = Specification::TestCase.new(merge_maps(h1, h2))
135
+ ex.bind(service, example.counterexample?)
136
+ end
137
+
138
+ def merge_maps(h1, h2)
139
+ h1.merge(h2) do |k,v1,v2|
140
+ case v1
141
+ when Hash then merge_maps(v1, v2)
142
+ when Array then v1 + v2
143
+ else v2
144
+ end
145
+ end
146
+ end
147
+
148
+ # Returns a proc that implements file_filter strategy according to the
149
+ # type of filter installed
150
+ def to_filter_proc(filter)
151
+ case ff = filter
152
+ when NilClass then ->(f){ true }
153
+ when Proc then ff
154
+ when Regexp then ->(f){ ff =~ f.to_s }
155
+ else
156
+ ->(f){ ff === f }
157
+ end
158
+ end
159
+
160
+ end # class Scope
161
+ end # class Configuration
162
+ end # module Webspicy
@@ -0,0 +1,58 @@
1
+ module Webspicy
2
+ class Configuration
3
+ class SingleUrl
4
+
5
+ class SingleUrlScope < Scope
6
+
7
+ def initialize(config, url)
8
+ super(config)
9
+ @url = url
10
+ end
11
+ attr_reader :url
12
+
13
+ def each_specification(&bl)
14
+ return enum_for(:each_specification) unless block_given?
15
+ spec = <<~YML
16
+ ---
17
+ name: |-
18
+ Default specification
19
+ url: |-
20
+ #{url}
21
+
22
+ services:
23
+ - method: |-
24
+ GET
25
+ description: |-
26
+ Getting #{url}
27
+
28
+ input_schema: |-
29
+ Any
30
+ output_schema: |-
31
+ Any
32
+ error_schema: |-
33
+ Any
34
+
35
+ examples:
36
+ - description: |-
37
+ it returns a 200
38
+ params: {}
39
+ expected:
40
+ status: 200
41
+ YML
42
+ Webspicy.debug(spec)
43
+ yield Webspicy.specification(spec, nil, self)
44
+ end
45
+
46
+ end # class SingleUrlScope
47
+
48
+ def initialize(url)
49
+ @url = url
50
+ end
51
+
52
+ def call(config)
53
+ SingleUrlScope.new(config, @url)
54
+ end
55
+
56
+ end # class SingleUrl
57
+ end # class Configuration
58
+ end # module Webspicy
@@ -0,0 +1,30 @@
1
+ module Webspicy
2
+ class Configuration
3
+ class SingleYmlFile
4
+
5
+ class SingleYmlFileScope < Scope
6
+
7
+ def initialize(config, file)
8
+ super(config)
9
+ @file = file
10
+ end
11
+ attr_reader :file
12
+
13
+ def each_specification(&bl)
14
+ return enum_for(:each_specification) unless block_given?
15
+ yield Webspicy.specification(file.read, nil, self)
16
+ end
17
+
18
+ end # class SingleYmlFileScope
19
+
20
+ def initialize(file)
21
+ @file = file
22
+ end
23
+
24
+ def call(config)
25
+ SingleYmlFileScope.new(config, @file)
26
+ end
27
+
28
+ end # class SingleYmlFile
29
+ end # class Configuration
30
+ end # module Webspicy
@@ -17,15 +17,30 @@ FileUpload =
17
17
  param_name :? String
18
18
  }
19
19
 
20
- Resource =
21
- .Webspicy::Resource <info> {
22
- name: String
23
- url: String
24
- services: [Service]
25
- }
20
+ Specification = .Webspicy::Specification
21
+ <singleservice> {
22
+ name :? String
23
+ url : String
24
+ method : Method
25
+ description : String
26
+ preconditions :? [String]|String
27
+ postconditions :? [String]|String
28
+ input_schema : Schema
29
+ output_schema : Schema
30
+ error_schema : Schema
31
+ blackbox :? String
32
+ default_example :? TestCase
33
+ examples :? [TestCase]
34
+ counterexamples :? [TestCase]
35
+ }
36
+ <info> {
37
+ name: String
38
+ url: String
39
+ services: [Service]
40
+ }
26
41
 
27
42
  Service =
28
- .Webspicy::Resource::Service <info> {
43
+ .Webspicy::Specification::Service <info> {
29
44
  method : Method
30
45
  description : String
31
46
  preconditions :? [String]|String
@@ -40,7 +55,7 @@ Service =
40
55
  }
41
56
 
42
57
  TestCase =
43
- .Webspicy::Resource::Service::TestCase <info> {
58
+ .Webspicy::Specification::TestCase <info> {
44
59
  description :? String
45
60
  dress_params :? Boolean
46
61
  params :? Params
@@ -29,8 +29,8 @@ module Webspicy
29
29
 
30
30
  def has_service?(path)
31
31
  config.each_scope do |scope|
32
- scope.each_resource do |resource|
33
- next unless url_matches?(resource, path)
32
+ scope.each_specification do |specification|
33
+ next unless url_matches?(specification, path)
34
34
  return true
35
35
  end
36
36
  end
@@ -39,9 +39,9 @@ module Webspicy
39
39
 
40
40
  def find_service(method, path)
41
41
  config.each_scope do |scope|
42
- scope.each_resource do |resource|
43
- next unless url_matches?(resource, path)
44
- scope.each_service(resource) do |service|
42
+ scope.each_specification do |specification|
43
+ next unless url_matches?(specification, path)
44
+ scope.each_service(specification) do |service|
45
45
  return service if service.method == method
46
46
  end
47
47
  end
@@ -71,8 +71,8 @@ module Webspicy
71
71
  end
72
72
  end
73
73
 
74
- def url_matches?(resource, path)
75
- resource.url_pattern.match(path)
74
+ def url_matches?(specification, path)
75
+ specification.url_pattern.match(path)
76
76
  end
77
77
 
78
78
  def random_body(service, request)
@@ -85,4 +85,4 @@ module Webspicy
85
85
  end
86
86
 
87
87
  end # class Mocker
88
- end # module Webspicy
88
+ end # module Webspicy
@@ -0,0 +1,5 @@
1
+ require "webspicy"
2
+ require "webspicy/mocker"
3
+ require 'rack'
4
+ config = Webspicy::Configuration.dress(Path("/formalspec/"))
5
+ run Webspicy::Mocker.new(config)
@@ -0,0 +1 @@
1
+ require_relative 'openapi/generator'
@@ -0,0 +1,127 @@
1
+ require "finitio/generation"
2
+ require "finitio/json_schema"
3
+ module Webspicy
4
+ module Openapi
5
+ class Generator
6
+
7
+ def initialize(config)
8
+ @config = Configuration.dress(config)
9
+ @generator = config.generator || Finitio::Generation.new
10
+ end
11
+ attr_reader :config, :generator
12
+
13
+ def call
14
+ {
15
+ openapi: "3.0.2",
16
+ info: {
17
+ version: "1.0.0",
18
+ title: "Hello API"
19
+ },
20
+ paths: paths
21
+ }
22
+ end
23
+
24
+ private
25
+
26
+ def paths
27
+ config.each_scope.inject({}) do |paths,scope|
28
+ scope.each_specification.inject(paths) do |paths,specification|
29
+ paths.merge(path_for(specification)){|k,ps,qs|
30
+ ps.merge(qs)
31
+ }
32
+ end
33
+ end
34
+ end
35
+
36
+ def path_for(specification)
37
+ {
38
+ specification.url => {
39
+ summary: specification.name
40
+ }.merge(verbs_for(specification))
41
+ }
42
+ end
43
+
44
+ def verbs_for(specification)
45
+ specification.services.inject({}) do |verbs,service|
46
+ verb = service.method.downcase
47
+ verb_defn = {
48
+ description: service.description,
49
+ responses: responses_for(service)
50
+ }
51
+ unless ["get", "options", "delete", "head"].include?(verb)
52
+ verb_defn[:requestBody] = request_body_for(service)
53
+ end
54
+ verbs.merge({ verb => verb_defn })
55
+ end
56
+ end
57
+
58
+ def request_body_for(service)
59
+ schema = actual_input_schema(service)
60
+ {
61
+ required: true,
62
+ content: {
63
+ "application/json" => {
64
+ schema: schema.to_json_schema,
65
+ example: generator.call(schema, {})
66
+ }
67
+ }
68
+ }
69
+ end
70
+
71
+ def responses_for(service)
72
+ result = {}
73
+ service.examples.each_with_object(result) do |test_case, rs|
74
+ rs.merge!(response_for(test_case, false)){|k,r1,r2| r1 }
75
+ end
76
+ service.counterexamples.each_with_object(result) do |test_case, rs|
77
+ rs.merge!(response_for(test_case, true)){|k,r1,r2| r1 }
78
+ end
79
+ result
80
+ end
81
+
82
+ def response_for(test_case, counterexample)
83
+ res = {
84
+ description: test_case.description,
85
+ }
86
+ status = (test_case.expected_status && test_case.expected_status.to_int) || 200
87
+ if test_case.expected_content_type && status != 204
88
+ content = {
89
+ schema: schema_for(test_case, counterexample)
90
+ }
91
+ unless counterexample
92
+ content[:example] = example_for(test_case, counterexample)
93
+ end
94
+ res[:content] = {
95
+ test_case.expected_content_type => content
96
+ }
97
+ end
98
+ {
99
+ status => res
100
+ }
101
+ end
102
+
103
+ def schema_for(test_case, counterexample)
104
+ schema = actual_output_schema(test_case, counterexample)
105
+ schema.to_json_schema
106
+ end
107
+
108
+ def example_for(test_case, counterexample)
109
+ schema = actual_output_schema(test_case, counterexample)
110
+ generator.call(schema, {})
111
+ end
112
+
113
+ def actual_output_schema(test_case, counterexample)
114
+ if counterexample
115
+ test_case.service.error_schema['Main']
116
+ else
117
+ test_case.service.output_schema['Main']
118
+ end
119
+ end
120
+
121
+ def actual_input_schema(service)
122
+ service.input_schema['Main']
123
+ end
124
+
125
+ end # class Generator
126
+ end # module Openapi
127
+ end # module Webspicy