json_spec 0.8.1 → 1.0.0

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 (39) hide show
  1. data/.travis.yml +2 -4
  2. data/Gemfile +1 -1
  3. data/{LICENSE.md → LICENSE} +0 -0
  4. data/README.md +221 -210
  5. data/features/files.feature +89 -0
  6. data/features/support/env.rb +3 -0
  7. data/features/types.feature +7 -1
  8. data/json_spec.gemspec +16 -20
  9. data/lib/json_spec/configuration.rb +10 -2
  10. data/lib/json_spec/cucumber.rb +18 -3
  11. data/lib/json_spec/errors.rb +22 -1
  12. data/lib/json_spec/helpers.rb +17 -2
  13. data/lib/json_spec/matchers/be_json_eql.rb +63 -0
  14. data/lib/json_spec/matchers/have_json_path.rb +30 -0
  15. data/lib/json_spec/matchers/have_json_size.rb +35 -0
  16. data/lib/json_spec/matchers/have_json_type.rb +49 -0
  17. data/lib/json_spec/matchers/include_json.rb +57 -0
  18. data/lib/json_spec/matchers.rb +9 -218
  19. data/lib/json_spec/memory.rb +2 -3
  20. data/lib/json_spec/messages.rb +8 -0
  21. data/lib/json_spec.rb +3 -1
  22. data/spec/json_spec/configuration_spec.rb +10 -0
  23. data/spec/json_spec/helpers_spec.rb +33 -1
  24. data/spec/json_spec/matchers/be_json_eql_spec.rb +109 -0
  25. data/spec/json_spec/matchers/have_json_path_spec.rb +29 -0
  26. data/spec/json_spec/matchers/have_json_size_spec.rb +47 -0
  27. data/spec/json_spec/matchers/have_json_type_spec.rb +89 -0
  28. data/spec/json_spec/matchers/include_json_spec.rb +76 -0
  29. data/spec/json_spec/matchers_spec.rb +43 -287
  30. data/spec/json_spec/memory_spec.rb +8 -3
  31. data/spec/spec_helper.rb +4 -0
  32. data/spec/support/files/one.json +1 -0
  33. data/spec/support/files/project/one.json +1 -0
  34. data/spec/support/files/project/two.json +18 -0
  35. data/spec/support/files/project/version/one.json +1 -0
  36. data/spec/support/files/project/version/two.json +3 -0
  37. data/spec/support/files/two.json +24 -0
  38. metadata +49 -20
  39. data/lib/json_spec/version.rb +0 -3
data/json_spec.gemspec CHANGED
@@ -1,26 +1,22 @@
1
- # -*- encoding: utf-8 -*-
2
- $:.push File.expand_path("../lib", __FILE__)
3
- require "json_spec/version"
1
+ # encoding: utf-8
4
2
 
5
- Gem::Specification.new do |s|
6
- s.name = "json_spec"
7
- s.version = JsonSpec::VERSION
8
- s.authors = ["Steve Richert"]
9
- s.email = ["steve.richert@gmail.com"]
10
- s.homepage = "https://github.com/collectiveidea/json_spec"
11
- s.summary = "Easily handle JSON in RSpec and Cucumber"
12
- s.description = "Easily handle JSON in RSpec and Cucumber"
3
+ Gem::Specification.new do |gem|
4
+ gem.name = "json_spec"
5
+ gem.version = "1.0.0"
13
6
 
14
- s.rubyforge_project = "json_spec"
7
+ gem.authors = ["Steve Richert"]
8
+ gem.email = ["steve.richert@gmail.com"]
9
+ gem.summary = "Easily handle JSON in RSpec and Cucumber"
10
+ gem.description = "Easily handle JSON in RSpec and Cucumber"
11
+ gem.homepage = "https://github.com/collectiveidea/json_spec"
15
12
 
16
- s.files = `git ls-files`.split("\n")
17
- s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
- s.executables = `git ls-files -- bin/*`.split("\n").map{|f| File.basename(f) }
19
- s.require_paths = ["lib"]
13
+ gem.add_dependency "multi_json", "~> 1.0"
14
+ gem.add_dependency "rspec", "~> 2.0"
20
15
 
21
- s.add_dependency "multi_json", "~> 1.0"
22
- s.add_dependency "rspec", "~> 2.0"
16
+ gem.add_development_dependency "cucumber", "~> 1.1", ">= 1.1.1"
17
+ gem.add_development_dependency "rake", "~> 0.9"
23
18
 
24
- s.add_development_dependency "rake", "~> 0.9"
25
- s.add_development_dependency "cucumber", "~> 1.1", ">= 1.1.1"
19
+ gem.files = `git ls-files`.split("\n")
20
+ gem.test_files = `git ls-files -- {spec,features}/*`.split("\n")
21
+ gem.require_paths = ["lib"]
26
22
  end
@@ -13,15 +13,23 @@ module JsonSpec
13
13
  end
14
14
 
15
15
  def excluded_keys=(keys)
16
- @excluded_keys = keys.map{|k| k.to_s }.uniq
16
+ @excluded_keys = keys.map(&:to_s).uniq
17
17
  end
18
18
 
19
19
  def exclude_keys(*keys)
20
20
  self.excluded_keys = keys
21
21
  end
22
22
 
23
+ def directory
24
+ @directory
25
+ end
26
+
27
+ def directory=(directory)
28
+ @directory = directory
29
+ end
30
+
23
31
  def reset
24
- instance_variables.each{|iv| remove_instance_variable(iv) }
32
+ instance_variables.each{|ivar| remove_instance_variable(ivar) }
25
33
  end
26
34
  end
27
35
  end
@@ -18,6 +18,14 @@ Then /^the (?:JSON|json)(?: response)?(?: at "(.*)")? should( not)? be:$/ do |pa
18
18
  end
19
19
  end
20
20
 
21
+ Then /^the (?:JSON|json)(?: response)?(?: at "(.*)")? should( not)? be file "(.+)"$/ do |path, negative, file_path|
22
+ if negative
23
+ last_json.should_not be_json_eql.to_file(file_path).at_path(path)
24
+ else
25
+ last_json.should be_json_eql.to_file(file_path).at_path(path)
26
+ end
27
+ end
28
+
21
29
  Then /^the (?:JSON|json)(?: response)?(?: at "(.*)")? should( not)? be (".*"|\-?\d+(?:\.\d+)?(?:[eE][\+\-]?\d+)?|\[.*\]|%?\{.*\}|true|false|null)$/ do |path, negative, value|
22
30
  if negative
23
31
  last_json.should_not be_json_eql(JsonSpec.remember(value)).at_path(path)
@@ -34,6 +42,14 @@ Then /^the (?:JSON|json)(?: response)?(?: at "(.*)")? should( not)? include:$/ d
34
42
  end
35
43
  end
36
44
 
45
+ Then /^the (?:JSON|json)(?: response)?(?: at "(.*)")? should( not)? include file "(.+)"$/ do |path, negative, file_path|
46
+ if negative
47
+ last_json.should_not include_json.from_file(file_path).at_path(path)
48
+ else
49
+ last_json.should include_json.from_file(file_path).at_path(path)
50
+ end
51
+ end
52
+
37
53
  Then /^the (?:JSON|json)(?: response)?(?: at "(.*)")? should( not)? include (".*"|\-?\d+(?:\.\d+)?(?:[eE][\+\-]?\d+)?|\[.*\]|%?\{.*\}|true|false|null)$/ do |path, negative, value|
38
54
  if negative
39
55
  last_json.should_not include_json(JsonSpec.remember(value)).at_path(path)
@@ -63,11 +79,10 @@ Then /^the (?:JSON|json)(?: response)? should( not)? have "(.*)"$/ do |negative,
63
79
  end
64
80
 
65
81
  Then /^the (?:JSON|json)(?: response)?(?: at "(.*)")? should( not)? be an? (.*)$/ do |path, negative, type|
66
- klass = Module.const_get(type.gsub(/^./){|x| x.upcase })
67
82
  if negative
68
- last_json.should_not have_json_type(klass).at_path(path)
83
+ last_json.should_not have_json_type(type).at_path(path)
69
84
  else
70
- last_json.should have_json_type(klass).at_path(path)
85
+ last_json.should have_json_type(type).at_path(path)
71
86
  end
72
87
  end
73
88
 
@@ -1,5 +1,8 @@
1
1
  module JsonSpec
2
- class MissingPathError < StandardError
2
+ class Error < StandardError
3
+ end
4
+
5
+ class MissingPath < Error
3
6
  attr_reader :path
4
7
 
5
8
  def initialize(path)
@@ -10,4 +13,22 @@ module JsonSpec
10
13
  %(Missing JSON path "#{path}")
11
14
  end
12
15
  end
16
+
17
+ class MissingDirectory < Error
18
+ def to_s
19
+ "No JsonSpec.directory set"
20
+ end
21
+ end
22
+
23
+ class MissingFile < Error
24
+ attr_reader :path
25
+
26
+ def initialize(path)
27
+ @path = path
28
+ end
29
+
30
+ def to_s
31
+ "No JSON file at #{path}"
32
+ end
33
+ end
13
34
  end
@@ -1,4 +1,4 @@
1
- require 'multi_json'
1
+ require "multi_json"
2
2
 
3
3
  module JsonSpec
4
4
  module Helpers
@@ -23,6 +23,13 @@ module JsonSpec
23
23
  end
24
24
  end
25
25
 
26
+ def load_json(relative_path)
27
+ missing_json_directory! if JsonSpec.directory.nil?
28
+ path = File.join(JsonSpec.directory, relative_path)
29
+ missing_json_file!(path) unless File.exist?(path)
30
+ File.read(path)
31
+ end
32
+
26
33
  private
27
34
  def value_at_json_path(ruby, path)
28
35
  return ruby unless path
@@ -40,7 +47,15 @@ module JsonSpec
40
47
  end
41
48
 
42
49
  def missing_json_path!(path)
43
- raise JsonSpec::MissingPathError.new(path)
50
+ raise JsonSpec::MissingPath.new(path)
51
+ end
52
+
53
+ def missing_json_directory!
54
+ raise JsonSpec::MissingDirectory
55
+ end
56
+
57
+ def missing_json_file!(path)
58
+ raise JsonSpec::MissingFile.new(path)
44
59
  end
45
60
  end
46
61
  end
@@ -0,0 +1,63 @@
1
+ module JsonSpec
2
+ module Matchers
3
+ class BeJsonEql
4
+ include JsonSpec::Helpers
5
+ include JsonSpec::Exclusion
6
+ include JsonSpec::Messages
7
+
8
+ attr_reader :expected, :actual
9
+
10
+ def diffable?
11
+ true
12
+ end
13
+
14
+ def initialize(expected_json = nil)
15
+ @expected_json = expected_json
16
+ end
17
+
18
+ def matches?(actual_json)
19
+ raise "Expected equivalent JSON not provided" if @expected_json.nil?
20
+
21
+ @actual, @expected = scrub(actual_json, @path), scrub(@expected_json)
22
+ @actual == @expected
23
+ end
24
+
25
+ def at_path(path)
26
+ @path = path
27
+ self
28
+ end
29
+
30
+ def to_file(path)
31
+ @expected_json = load_json(path)
32
+ self
33
+ end
34
+
35
+ def excluding(*keys)
36
+ excluded_keys.merge(keys.map(&:to_s))
37
+ self
38
+ end
39
+
40
+ def including(*keys)
41
+ excluded_keys.subtract(keys.map(&:to_s))
42
+ self
43
+ end
44
+
45
+ def failure_message_for_should
46
+ message_with_path("Expected equivalent JSON")
47
+ end
48
+
49
+ def failure_message_for_should_not
50
+ message_with_path("Expected inequivalent JSON")
51
+ end
52
+
53
+ def description
54
+ message_with_path("equal JSON")
55
+ end
56
+
57
+ private
58
+ def scrub(json, path = nil)
59
+ generate_normalized_json(exclude_keys(parse_json(json, path))).chomp + "\n"
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,30 @@
1
+ module JsonSpec
2
+ module Matchers
3
+ class HaveJsonPath
4
+ include JsonSpec::Helpers
5
+
6
+ def initialize(path)
7
+ @path = path
8
+ end
9
+
10
+ def matches?(json)
11
+ parse_json(json, @path)
12
+ true
13
+ rescue JsonSpec::MissingPath
14
+ false
15
+ end
16
+
17
+ def failure_message_for_should
18
+ %(Expected JSON path "#{@path}")
19
+ end
20
+
21
+ def failure_message_for_should_not
22
+ %(Expected no JSON path "#{@path}")
23
+ end
24
+
25
+ def description
26
+ %(have JSON path "#{@path}")
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,35 @@
1
+ module JsonSpec
2
+ module Matchers
3
+ class HaveJsonSize
4
+ include JsonSpec::Helpers
5
+ include JsonSpec::Messages
6
+
7
+ def initialize(size)
8
+ @expected = size
9
+ end
10
+
11
+ def matches?(json)
12
+ ruby = parse_json(json, @path)
13
+ @actual = Enumerable === ruby ? ruby.size : 1
14
+ @actual == @expected
15
+ end
16
+
17
+ def at_path(path)
18
+ @path = path
19
+ self
20
+ end
21
+
22
+ def failure_message_for_should
23
+ message_with_path("Expected JSON value size to be #{@expected}, got #{@actual}")
24
+ end
25
+
26
+ def failure_message_for_should_not
27
+ message_with_path("Expected JSON value size to not be #{@expected}, got #{@actual}")
28
+ end
29
+
30
+ def description
31
+ message_with_path(%(have JSON size "#{@expected}"))
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,49 @@
1
+ module JsonSpec
2
+ module Matchers
3
+ class HaveJsonType
4
+ include JsonSpec::Helpers
5
+ include JsonSpec::Messages
6
+
7
+ def initialize(type)
8
+ @classes = type_to_classes(type)
9
+ end
10
+
11
+ def matches?(json)
12
+ @ruby = parse_json(json, @path)
13
+ @classes.any?{|c| c === @ruby }
14
+ end
15
+
16
+ def at_path(path)
17
+ @path = path
18
+ self
19
+ end
20
+
21
+ def failure_message_for_should
22
+ message_with_path("Expected JSON value type to be #{@classes.join(", ")}, got #{@ruby.class}")
23
+ end
24
+
25
+ def failure_message_for_should_not
26
+ message_with_path("Expected JSON value type to not be #{@classes.join(", ")}, got #{@ruby.class}")
27
+ end
28
+
29
+ def description
30
+ message_with_path(%(have JSON type "#{@classes.join(", ")}"))
31
+ end
32
+
33
+ private
34
+ def type_to_classes(type)
35
+ case type
36
+ when Class then [type]
37
+ when Array then type.map{|t| type_to_classes(t) }.flatten
38
+ else
39
+ case type.to_s.downcase
40
+ when "boolean" then [TrueClass, FalseClass]
41
+ when "object" then [Hash]
42
+ when "nil", "null" then [NilClass]
43
+ else [Module.const_get(type.to_s.capitalize)]
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,57 @@
1
+ module JsonSpec
2
+ module Matchers
3
+ class IncludeJson
4
+ include JsonSpec::Helpers
5
+ include JsonSpec::Exclusion
6
+ include JsonSpec::Messages
7
+
8
+ def initialize(expected_json = nil)
9
+ @expected_json = expected_json
10
+ end
11
+
12
+ def matches?(actual_json)
13
+ raise "Expected included JSON not provided" if @expected_json.nil?
14
+
15
+ actual = parse_json(actual_json, @path)
16
+ expected = exclude_keys(parse_json(@expected_json))
17
+ case actual
18
+ when Hash then actual.values.map{|v| exclude_keys(v) }.include?(expected)
19
+ when Array then actual.map{|e| exclude_keys(e) }.include?(expected)
20
+ else false
21
+ end
22
+ end
23
+
24
+ def at_path(path)
25
+ @path = path
26
+ self
27
+ end
28
+
29
+ def from_file(path)
30
+ @expected_json = load_json(path)
31
+ self
32
+ end
33
+
34
+ def excluding(*keys)
35
+ excluded_keys.merge(keys.map(&:to_s))
36
+ self
37
+ end
38
+
39
+ def including(*keys)
40
+ excluded_keys.subtract(keys.map(&:to_s))
41
+ self
42
+ end
43
+
44
+ def failure_message_for_should
45
+ message_with_path("Expected included JSON")
46
+ end
47
+
48
+ def failure_message_for_should_not
49
+ message_with_path("Expected excluded JSON")
50
+ end
51
+
52
+ def description
53
+ message_with_path("include JSON")
54
+ end
55
+ end
56
+ end
57
+ end
@@ -1,225 +1,16 @@
1
- require "json"
2
- require "rspec"
1
+ require "json_spec/matchers/be_json_eql"
2
+ require "json_spec/matchers/include_json"
3
+ require "json_spec/matchers/have_json_path"
4
+ require "json_spec/matchers/have_json_type"
5
+ require "json_spec/matchers/have_json_size"
3
6
 
4
7
  module JsonSpec
5
8
  module Matchers
6
- class BeJsonEql
7
- include JsonSpec::Helpers
8
- include JsonSpec::Exclusion
9
-
10
- attr_reader :expected, :actual
11
-
12
- def diffable?
13
- true
14
- end
15
-
16
- def initialize(expected_json)
17
- @expected_json = expected_json
18
- end
19
-
20
- def matches?(actual_json)
21
- @actual, @expected = scrub(actual_json, @path), scrub(@expected_json)
22
- @actual == @expected
23
- end
24
-
25
- def at_path(path)
26
- @path = path
27
- self
28
- end
29
-
30
- def excluding(*keys)
31
- excluded_keys.merge(keys.map{|k| k.to_s })
32
- self
33
- end
34
-
35
- def including(*keys)
36
- excluded_keys.subtract(keys.map{|k| k.to_s })
37
- self
38
- end
39
-
40
- def failure_message_for_should
41
- message = "Expected equivalent JSON"
42
- message << %( at path "#{@path}") if @path
43
- message
44
- end
45
-
46
- def failure_message_for_should_not
47
- message = "Expected inequivalent JSON"
48
- message << %( at path "#{@path}") if @path
49
- message
50
- end
51
-
52
- def description
53
- message = "equal JSON"
54
- message << %( at path "#{@path}") if @path
55
- message
56
- end
57
-
58
- private
59
- def scrub(json, path = nil)
60
- generate_normalized_json(exclude_keys(parse_json(json, path))).chomp + "\n"
61
- end
62
- end
63
-
64
- class IncludeJson
65
- include JsonSpec::Helpers
66
- include JsonSpec::Exclusion
67
-
68
- def initialize(expected_json)
69
- @expected_json = expected_json
70
- end
71
-
72
- def matches?(actual_json)
73
- actual = parse_json(actual_json, @path)
74
- expected = exclude_keys(parse_json(@expected_json))
75
- case actual
76
- when Hash then actual.values.map{|v| exclude_keys(v) }.include?(expected)
77
- when Array then actual.map{|e| exclude_keys(e) }.include?(expected)
78
- else false
79
- end
80
- end
81
-
82
- def at_path(path)
83
- @path = path
84
- self
85
- end
86
-
87
- def excluding(*keys)
88
- excluded_keys.merge(keys.map{|k| k.to_s })
89
- self
90
- end
91
-
92
- def including(*keys)
93
- excluded_keys.subtract(keys.map{|k| k.to_s })
94
- self
95
- end
96
-
97
- def failure_message_for_should
98
- message = "Expected included JSON"
99
- message << %( at path "#{@path}") if @path
100
- message
101
- end
102
-
103
- def failure_message_for_should_not
104
- message = "Expected excluded JSON"
105
- message << %( at path "#{@path}") if @path
106
- message
107
- end
108
-
109
- def description
110
- message = "include JSON"
111
- message << %( at path "#{@path}") if @path
112
- message
113
- end
114
- end
115
-
116
- class HaveJsonPath
117
- include JsonSpec::Helpers
118
-
119
- def initialize(path)
120
- @path = path
121
- end
122
-
123
- def matches?(json)
124
- begin
125
- parse_json(json, @path)
126
- true
127
- rescue JsonSpec::MissingPathError
128
- false
129
- end
130
- end
131
-
132
- def failure_message_for_should
133
- %(Expected JSON path "#{@path}")
134
- end
135
-
136
- def failure_message_for_should_not
137
- %(Expected no JSON path "#{@path}")
138
- end
139
-
140
- def description
141
- %(have JSON path "#{@path}")
142
- end
143
- end
144
-
145
- class HaveJsonType
146
- include JsonSpec::Helpers
147
-
148
- def initialize(klass)
149
- @klass = klass
150
- end
151
-
152
- def matches?(json)
153
- @ruby = parse_json(json, @path)
154
- @ruby.is_a?(@klass)
155
- end
156
-
157
- def at_path(path)
158
- @path = path
159
- self
160
- end
161
-
162
- def failure_message_for_should
163
- message = "Expected JSON value type to be #{@klass}, got #{@ruby.class}"
164
- message << %( at path "#{@path}") if @path
165
- message
166
- end
167
-
168
- def failure_message_for_should_not
169
- message = "Expected JSON value type to not be #{@klass}, got #{@ruby.class}"
170
- message << %( at path "#{@path}") if @path
171
- message
172
- end
173
-
174
- def description
175
- message = %(have JSON type "#{@klass.to_s}")
176
- message << %( at path "#{@path}") if @path
177
- message
178
- end
179
- end
180
-
181
- class HaveJsonSize
182
- include JsonSpec::Helpers
183
-
184
- def initialize(size)
185
- @expected = size
186
- end
187
-
188
- def matches?(json)
189
- ruby = parse_json(json, @path)
190
- @actual = ruby.is_a?(Enumerable) ? ruby.size : 1
191
- @actual == @expected
192
- end
193
-
194
- def at_path(path)
195
- @path = path
196
- self
197
- end
198
-
199
- def failure_message_for_should
200
- message = "Expected JSON value size to be #{@expected}, got #{@actual}"
201
- message << %( at path "#{@path}") if @path
202
- message
203
- end
204
-
205
- def failure_message_for_should_not
206
- message = "Expected JSON value size to not be #{@expected}, got #{@actual}"
207
- message << %( at path "#{@path}") if @path
208
- message
209
- end
210
-
211
- def description
212
- message = %(have JSON size "#{@expected}")
213
- message << %( at path "#{@path}") if @path
214
- message
215
- end
216
- end
217
-
218
- def be_json_eql(json)
9
+ def be_json_eql(json = nil)
219
10
  JsonSpec::Matchers::BeJsonEql.new(json)
220
11
  end
221
12
 
222
- def include_json(json)
13
+ def include_json(json = nil)
223
14
  JsonSpec::Matchers::IncludeJson.new(json)
224
15
  end
225
16
 
@@ -227,8 +18,8 @@ module JsonSpec
227
18
  JsonSpec::Matchers::HaveJsonPath.new(path)
228
19
  end
229
20
 
230
- def have_json_type(klass)
231
- JsonSpec::Matchers::HaveJsonType.new(klass)
21
+ def have_json_type(type)
22
+ JsonSpec::Matchers::HaveJsonType.new(type)
232
23
  end
233
24
 
234
25
  def have_json_size(size)
@@ -5,12 +5,11 @@ module JsonSpec
5
5
  end
6
6
 
7
7
  def memorize(key, value)
8
- memory[key] = value
8
+ memory[key.to_sym] = value
9
9
  end
10
10
 
11
11
  def remember(json)
12
- return json if memory.empty?
13
- json.gsub(/%\{(#{memory.keys.map{|k| Regexp.quote(k) }.join("|")})\}/){ memory[$1] }
12
+ memory.empty? ? json : json % memory
14
13
  end
15
14
 
16
15
  def forget
@@ -0,0 +1,8 @@
1
+ module JsonSpec
2
+ module Messages
3
+ def message_with_path(message)
4
+ message << %( at path "#{@path}") if @path
5
+ message
6
+ end
7
+ end
8
+ end
data/lib/json_spec.rb CHANGED
@@ -1,8 +1,10 @@
1
- require "json_spec/version"
1
+ require "json"
2
+ require "rspec"
2
3
  require "json_spec/errors"
3
4
  require "json_spec/configuration"
4
5
  require "json_spec/exclusion"
5
6
  require "json_spec/helpers"
7
+ require "json_spec/messages"
6
8
  require "json_spec/matchers"
7
9
  require "json_spec/memory"
8
10