openapi3_parser 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +10 -1
  3. data/CHANGELOG.md +5 -0
  4. data/README.md +13 -0
  5. data/TODO.md +11 -8
  6. data/lib/openapi3_parser.rb +16 -28
  7. data/lib/openapi3_parser/context.rb +92 -34
  8. data/lib/openapi3_parser/context/location.rb +37 -0
  9. data/lib/openapi3_parser/context/pointer.rb +34 -0
  10. data/lib/openapi3_parser/document.rb +115 -15
  11. data/lib/openapi3_parser/document/reference_register.rb +50 -0
  12. data/lib/openapi3_parser/error.rb +27 -1
  13. data/lib/openapi3_parser/node/object.rb +26 -1
  14. data/lib/openapi3_parser/node_factories/array.rb +15 -14
  15. data/lib/openapi3_parser/node_factories/callback.rb +2 -1
  16. data/lib/openapi3_parser/node_factories/link.rb +1 -1
  17. data/lib/openapi3_parser/node_factories/map.rb +13 -11
  18. data/lib/openapi3_parser/node_factories/openapi.rb +2 -0
  19. data/lib/openapi3_parser/node_factories/path_item.rb +13 -18
  20. data/lib/openapi3_parser/node_factories/paths.rb +2 -1
  21. data/lib/openapi3_parser/node_factories/reference.rb +7 -9
  22. data/lib/openapi3_parser/node_factories/responses.rb +4 -2
  23. data/lib/openapi3_parser/node_factories/security_requirement.rb +2 -1
  24. data/lib/openapi3_parser/node_factory.rb +39 -30
  25. data/lib/openapi3_parser/node_factory/fields/reference.rb +44 -0
  26. data/lib/openapi3_parser/node_factory/map.rb +5 -5
  27. data/lib/openapi3_parser/node_factory/object.rb +6 -5
  28. data/lib/openapi3_parser/node_factory/object/node_builder.rb +12 -11
  29. data/lib/openapi3_parser/node_factory/object/validator.rb +25 -14
  30. data/lib/openapi3_parser/nodes/components.rb +9 -11
  31. data/lib/openapi3_parser/nodes/discriminator.rb +1 -1
  32. data/lib/openapi3_parser/nodes/encoding.rb +1 -1
  33. data/lib/openapi3_parser/nodes/link.rb +1 -1
  34. data/lib/openapi3_parser/nodes/media_type.rb +2 -2
  35. data/lib/openapi3_parser/nodes/oauth_flow.rb +1 -1
  36. data/lib/openapi3_parser/nodes/openapi.rb +3 -4
  37. data/lib/openapi3_parser/nodes/operation.rb +5 -8
  38. data/lib/openapi3_parser/nodes/parameter/parameter_like.rb +2 -2
  39. data/lib/openapi3_parser/nodes/path_item.rb +2 -3
  40. data/lib/openapi3_parser/nodes/request_body.rb +1 -1
  41. data/lib/openapi3_parser/nodes/response.rb +3 -3
  42. data/lib/openapi3_parser/nodes/schema.rb +6 -7
  43. data/lib/openapi3_parser/nodes/server.rb +1 -2
  44. data/lib/openapi3_parser/nodes/server_variable.rb +1 -1
  45. data/lib/openapi3_parser/source.rb +136 -0
  46. data/lib/openapi3_parser/source/reference.rb +68 -0
  47. data/lib/openapi3_parser/source/reference_resolver.rb +81 -0
  48. data/lib/openapi3_parser/source_input.rb +71 -0
  49. data/lib/openapi3_parser/source_input/file.rb +102 -0
  50. data/lib/openapi3_parser/source_input/raw.rb +90 -0
  51. data/lib/openapi3_parser/source_input/resolve_next.rb +62 -0
  52. data/lib/openapi3_parser/source_input/string_parser.rb +43 -0
  53. data/lib/openapi3_parser/source_input/url.rb +123 -0
  54. data/lib/openapi3_parser/validation/error.rb +36 -3
  55. data/lib/openapi3_parser/validation/error_collection.rb +57 -15
  56. data/lib/openapi3_parser/validators/reference.rb +40 -0
  57. data/lib/openapi3_parser/version.rb +1 -1
  58. data/openapi3_parser.gemspec +10 -5
  59. metadata +34 -20
  60. data/lib/openapi3_parser/fields/map.rb +0 -83
  61. data/lib/openapi3_parser/node.rb +0 -115
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f60ae34f39675f7ce3616bd96dc3e383275d117d
4
- data.tar.gz: 4ecf6bffd69c875778c4d096f29cf7b5deadbf7e
3
+ metadata.gz: 651f457272264ca4fc7b45040203a9fa7deeabde
4
+ data.tar.gz: b00561ee20a2c121d9f1d030511b07870cd66ecc
5
5
  SHA512:
6
- metadata.gz: f5a96679f760117e345c9e992161444c06df7d667953fad6badf547dd83dee449b672cc9f265a26a7166e2cdec887d5205bf0dcda6abdbece2a2ce74aeb61153
7
- data.tar.gz: 087aea7ab5f4abceb69b9acce6854f70dac44fca1620bde695d7dd1a7813ca1a468728e3cc5a08ab5ae31d5320c75459f9537ce584c6ffbff082ca796d9a3602
6
+ metadata.gz: a38facc684d56b0194066ba88db7396107d882bc225fa07f14600bf21a5156c2f6032f8b284bac5146a6f3a9e792147e63fd35ca915b8b8d2f36fe57e738a662
7
+ data.tar.gz: f605fd5997afe6055b6e627eaca9971dd60dd6f2fbc19381a363490a596f46cf001317aed21130c4273115143c6353516c035747ab16867b62ece619434b6b1a
@@ -1,4 +1,13 @@
1
1
  language: ruby
2
2
  sudo: false
3
3
  rvm:
4
- - 2.3.1
4
+ - 2.3.6
5
+ - 2.4.3
6
+ - jruby-9.1.15.0
7
+ - ruby-head
8
+ - jruby-head
9
+ matrix:
10
+ allow_failures:
11
+ - rvm: ruby-head
12
+ - rvm: jruby-head
13
+ fast_finish: true
@@ -1,3 +1,8 @@
1
+ # 0.3.0
2
+
3
+ - Allow opening files by URL
4
+ - Support references in different files
5
+
1
6
  # 0.2.0
2
7
 
3
8
  - Allow defaulting to empty arrays and maps
data/README.md CHANGED
@@ -25,6 +25,19 @@ Documentation for the API to navigate the OpenAPI nodes is available on
25
25
  [openapi-3]: https://github.com/OAI/OpenAPI-Specification
26
26
  [docs]: http://www.rubydoc.info/github/kevindew/openapi3_parser/Openapi3Parser/Nodes/Openapi
27
27
 
28
+ ## Installation
29
+
30
+ You can install this gem into your bundler application by adding this line to
31
+ your Gemfile:
32
+
33
+ ```
34
+ gem "openapi3_parser", "~> 0.2.0"
35
+ ```
36
+
37
+ and then running `$ bundle install`
38
+
39
+ Or install the gem onto your machine via ` $ gem install openapi3_parser`
40
+
28
41
  ## Status
29
42
 
30
43
  This is currently a work in progress and will remain so until it reaches 1.0.
data/TODO.md CHANGED
@@ -6,26 +6,29 @@ These are the steps defined to reach 1.0. Assistance is very welcome.
6
6
  - [ ] Refactor the various NodeFactory modules to be a less confusing
7
7
  hierachical structure. Consider having factories subclass instead of use
8
8
  mixin
9
- - [ ] Decouple Document class for the source file. Consider a source file class
9
+ - [x] Decouple Document class for the source file. Consider a source file class
10
10
  instead
11
- - [ ] Validate that a reference creates the type of node that is expected in
11
+ - [x] Validate that a reference creates the type of node that is expected in
12
12
  a context
13
- - [ ] Allow opening of references from additional files
14
- - [ ] Allow opening of openapi documents by URL
15
- - [ ] Support references by URL, consider option to limit behaviour
13
+ - [x] Allow opening of references from additional files
14
+ - [x] Allow opening of openapi documents by URL
15
+ - [x] Support references by URL
16
+ - [ ] Consider option to limit open by URL/path behaviour
16
17
  - [ ] Support converting CommonMark to HTML
17
18
  - [ ] Reach parity with OpenAPI specification for validation
18
19
  - [ ] Consider a lenient mode for a document to only have to comply with type
19
20
  based validation
20
21
  - [ ] Improve test coverage
21
22
  - [ ] Publish documentation of the interface through the structure
22
- - [ ] Consider a resolved context class for representing context with a node
23
+ - [x] Consider a resolved context class for representing context with a node
23
24
  that can handle scenarios where a node is represented by both a reference
24
25
  and resolved context
25
26
  - [ ] Create error classes for various scenarios
26
27
  - [ ] Associate/resolve operation id / operation references
27
28
  - [ ] Do something to model expressions
28
- - [ ] Improve the modelling of namespace
29
+ - [x] Improve the modelling of namespace
29
30
  - [ ] Set up nicer string representations of key classes to help them be
30
31
  debugged
31
- - [ ] Ensure Array and Map nodes return empty ones by default rather than nil
32
+ - [x] Ensure Array and Map nodes return empty ones by default rather than nil
33
+ - [ ] Make JSON pointer public access to be consistent accepting string, array
34
+ or (potentially) a pointer class
@@ -1,52 +1,40 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "openapi3_parser/error"
4
3
  require "openapi3_parser/document"
5
-
6
- require "yaml"
7
- require "json"
4
+ require "openapi3_parser/source_input/raw"
5
+ require "openapi3_parser/source_input/file"
6
+ require "openapi3_parser/source_input/url"
8
7
 
9
8
  module Openapi3Parser
10
9
  # For a variety of inputs this will construct an OpenAPI document. For a
11
10
  # String/File input it will try to determine if the input is JSON or YAML.
12
11
  #
13
- # @param [String, Hash, File] input Source for the OpenAPI document
12
+ # @param [String, Hash, File] input Source for the OpenAPI document
14
13
  #
15
14
  # @return [Document]
16
15
  def self.load(input)
17
- # working_directory ||= if input.respond_to?(:read)
18
- # File.dirname(input)
19
- # else
20
- # Dir.pwd
21
- # end
22
-
23
- Document.new(parse_input(input))
16
+ Document.new(SourceInput::Raw.new(input))
24
17
  end
25
18
 
26
19
  # For a given string filename this will read the file and parse it as an
27
20
  # OpenAPI document. It will try detect automatically whether the contents
28
21
  # are JSON or YAML.
29
22
  #
30
- # @param [String] path Filename of the OpenAPI document
23
+ # @param [String] path Filename of the OpenAPI document
31
24
  #
32
25
  # @return [Document]
33
26
  def self.load_file(path)
34
- file = File.open(path)
35
- load(file)
27
+ Document.new(SourceInput::File.new(path))
36
28
  end
37
29
 
38
- def self.parse_input(input)
39
- return input if input.respond_to?(:keys)
40
-
41
- extension = input.respond_to?(:extname) ? input.extname : nil
42
- contents = input.respond_to?(:read) ? input.read : input
43
-
44
- if extension == ".json" || contents.strip[0] == "{"
45
- JSON.parse(contents)
46
- else
47
- YAML.safe_load(contents, [], [], true)
48
- end
30
+ # For a given string URL this will request the resource and parse it as an
31
+ # OpenAPI document. It will try detect automatically whether the contents
32
+ # are JSON or YAML.
33
+ #
34
+ # @param [String] url URL of the OpenAPI document
35
+ #
36
+ # @return [Document]
37
+ def self.load_url(url)
38
+ Document.new(SourceInput::Url.new(url.to_s))
49
39
  end
50
-
51
- private_class_method :parse_input
52
40
  end
@@ -1,54 +1,112 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "openapi3_parser/context/location"
4
+
3
5
  module Openapi3Parser
6
+ # Context is a construct used in both the node factories and the nodes
7
+ # themselves. It is used to represent the data, and the source of it, that
8
+ # a node is associated with. It also acts as a bridge between a node/node
9
+ # factory and associated document.
10
+ #
11
+ # @attr_reader input
12
+ # @attr_reader [Context::Location] document_location
13
+ # @attr_reader [Context::Location, nil] source_location
14
+ # @attr_reader [Context, nil] referenced_by
4
15
  class Context
5
- attr_reader :input, :namespace, :document, :parent
16
+ # Create a context for the root of a document
17
+ # @return [Context]
18
+ def self.root(input, source)
19
+ location = Location.new(source, [])
20
+ new(input, document_location: location)
21
+ end
22
+
23
+ # Create a context for a field within the current contexts data
24
+ # eg for a context of:
25
+ # root = Context.root({ "test" => {} }, source)
26
+ # we can get the context of "test" with:
27
+ # test = Context.next_field(root, "test")
28
+ #
29
+ # @param [Context] parent_context
30
+ # @param [String] field
31
+ # @return [Context]
32
+ def self.next_field(parent_context, field)
33
+ pc = parent_context
34
+ input = pc.input.respond_to?(:[]) ? pc.input[field] : nil
35
+ new(input,
36
+ document_location: Location.next_field(pc.document_location, field),
37
+ source_location: Location.next_field(pc.source_location, field),
38
+ referenced_by: pc.referenced_by)
39
+ end
40
+
41
+ # Creates the context for a field that is referenced by a context.
42
+ # In this scenario the context of the document is the same but we are in
43
+ # a different part of the source file, or even a different source file
44
+ #
45
+ # @param [Context] referencer_context
46
+ # @param input
47
+ # @param [Source] source
48
+ # @param [::Array] pointer_segments
49
+ # @return [Context]
50
+ def self.reference_field(referencer_context,
51
+ input:,
52
+ source:,
53
+ pointer_segments:)
54
+ new(input,
55
+ document_location: referencer_context.document_location,
56
+ source_location: Location.new(source, pointer_segments),
57
+ referenced_by: referencer_context)
58
+ end
59
+
60
+ attr_reader :input, :document_location, :source_location, :referenced_by
6
61
 
7
- def initialize(input:, namespace: [], document:, parent: nil)
62
+ # @param input
63
+ # @param [Context::Location] document_location
64
+ # @param [Context::Location, nil] source_location
65
+ # @param [Context, nil] referenced_by
66
+ def initialize(input,
67
+ document_location:,
68
+ source_location: nil,
69
+ referenced_by: nil)
8
70
  @input = input
9
- @namespace = namespace.freeze
10
- @document = document
11
- @parent = parent
71
+ @document_location = document_location
72
+ @source_location = source_location || document_location
73
+ @referenced_by = referenced_by
12
74
  end
13
75
 
14
- def self.root(input, document)
15
- new(input: input, document: document)
76
+ # @return [Document]
77
+ def document
78
+ document_location.source.document
16
79
  end
17
80
 
18
- def stringify_namespace
19
- return "root" if namespace.empty?
20
- namespace
21
- .map { |i| i.to_s.include?("/") ? %("#{i}") : i }
22
- .join("/")
81
+ # @return [Source]
82
+ def source
83
+ source_location.source
23
84
  end
24
85
 
25
- def next_namespace(segment, next_input = nil)
26
- next_input ||= input.nil? ? nil : input[segment]
27
- self.class.new(
28
- input: next_input,
29
- namespace: namespace + [segment],
30
- document: document,
31
- parent: self
32
- )
86
+ # @return [Source::ReferenceResolver]
87
+ def register_reference(reference, factory)
88
+ source.register_reference(reference, factory, self)
33
89
  end
34
90
 
35
- def resolve_reference
36
- document.resolve_reference(input["$ref"]) do |resolved_input, namespace|
37
- # @TODO track reference for cyclic depenendies
38
- next_context = resolved_reference(resolved_input, namespace)
39
- yield(next_context)
40
- end
91
+ # @deprecated
92
+ def namespace
93
+ document_location.pointer.segments
41
94
  end
42
95
 
43
- private
96
+ def inspect
97
+ %{#{self.class.name}(document_location: #{document_location}, } +
98
+ %{source_location: #{source_location}), referenced_by: } +
99
+ %{#{referenced_by})}
100
+ end
101
+
102
+ def location_summary
103
+ summary = document_location.to_s
104
+ summary += " (#{source_location})" if document_location != source_location
105
+ summary
106
+ end
44
107
 
45
- def resolved_reference(input, namespace)
46
- self.class.new(
47
- input: input,
48
- namespace: namespace,
49
- document: document,
50
- parent: parent
51
- )
108
+ def to_s
109
+ location_summary
52
110
  end
53
111
  end
54
112
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openapi3_parser/context/pointer"
4
+
5
+ module Openapi3Parser
6
+ class Context
7
+ # Class used to represent a location within an OpenAPI document.
8
+ # It contains a source, which is the source file/data used for the contents
9
+ # and the pointer which indicates where in the object like file the data is
10
+ class Location
11
+ def self.next_field(location, field)
12
+ new(location.source, location.pointer.segments + [field])
13
+ end
14
+
15
+ attr_reader :source, :pointer
16
+
17
+ # @param [Openapi3Parser::Source] source
18
+ # @param [::Array] pointer_segments
19
+ def initialize(source, pointer_segments)
20
+ @source = source
21
+ @pointer = Pointer.new(pointer_segments.freeze)
22
+ end
23
+
24
+ def ==(other)
25
+ source == other.source && pointer == other.pointer
26
+ end
27
+
28
+ def to_s
29
+ source.relative_to_root + pointer.fragment
30
+ end
31
+
32
+ def inspect
33
+ %{#{self.class.name}(source: #{source.inspect}, pointer: #{pointer})}
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Openapi3Parser
4
+ class Context
5
+ # A class to decorate the array of fields that make up a pointer and
6
+ # provide common means to convert it into different representations.
7
+ class Pointer
8
+ attr_reader :segments
9
+
10
+ # @param [::Array] segments
11
+ def initialize(segments)
12
+ @segments = segments.freeze
13
+ end
14
+
15
+ def ==(other)
16
+ segments == other.segments
17
+ end
18
+
19
+ def fragment
20
+ segments.map { |s| CGI.escape(s.to_s).gsub("+", "%20") }
21
+ .join("/")
22
+ .prepend("#/")
23
+ end
24
+
25
+ def to_s
26
+ fragment
27
+ end
28
+
29
+ def inspect
30
+ %{#{self.class.name}(segments: #{segments}, fragment: "#{fragment}")}
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,48 +1,148 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "openapi3_parser/context"
4
+ require "openapi3_parser/document/reference_register"
4
5
  require "openapi3_parser/error"
5
6
  require "openapi3_parser/node_factories/openapi"
7
+ require "openapi3_parser/source"
8
+ require "openapi3_parser/validation/error_collection"
6
9
 
7
10
  require "forwardable"
8
11
 
9
12
  module Openapi3Parser
13
+ # Document is the root construct of a created OpenAPI Document and can be
14
+ # used to navigate the contents of a document or to check it's validity.
15
+ #
16
+ # @attr_reader [Source] root_source
10
17
  class Document
11
18
  extend Forwardable
19
+ include Enumerable
12
20
 
13
- attr_reader :input
21
+ attr_reader :root_source
14
22
 
15
- def_delegators :factory, :valid?, :errors
23
+ # @!method valid?
24
+ # Whether this OpenAPI document has any validation issues or not. See
25
+ # #errors to access the errors
26
+ #
27
+ # @return [Boolean]
28
+ def_delegator :factory, :valid?
29
+
30
+ # @!method openapi
31
+ # The value of the openapi version field for this document
32
+ # @see Nodes::Openapi#openapi
33
+ # @return [String]
34
+ # @!method info
35
+ # The value of the info field on the OpenAPI document
36
+ # @see Nodes::Openapi#info
37
+ # @return [Nodes::Info]
38
+ # @!method servers
39
+ # The value of the servers field on the OpenAPI document
40
+ # @see Nodes::Openapi#servers
41
+ # @return [Nodes::Array<Nodes::Server>]
42
+ # @!method paths
43
+ # The value of the paths field on the OpenAPI document
44
+ # @see Nodes::Openapi#paths
45
+ # @return [Nodes::Paths]
46
+ # @!method components
47
+ # The value of the components field on the OpenAPI document
48
+ # @see Nodes::Openapi#components
49
+ # @return [Nodes::Components]
50
+ # @!method security
51
+ # The value of the security field on the OpenAPI document
52
+ # @see Nodes::Openapi#security
53
+ # @return [Nodes::Array<Nodes::SecurityRequirement>]
54
+ # @!method tags
55
+ # The value of the tags field on the OpenAPI document
56
+ # @see Nodes::Openapi#tags
57
+ # @return [Nodes::Array<Nodes::Tag>]
58
+ # @!method external_docs
59
+ # The value of the external_docs field on the OpenAPI document
60
+ # @see Nodes::Openapi#external_docs
61
+ # @return [Nodes::ExternalDocumentation]
62
+ # @!method extension
63
+ # Look up an extension field provided for the root object of the document
64
+ # @see Node::Object#extension
65
+ # @return [Hash, Array, Numeric, String, true, false, nil]
66
+ # @!method []
67
+ # Look up an attribute on the root of the OpenAPI document by String
68
+ # or Symbol
69
+ # @see Node::Object#[]
70
+ # @return Object
71
+ # @!method each
72
+ # Iterate through the attributes of the root object
73
+ # @see Node::Object#each
16
74
  def_delegators :root, :openapi, :info, :servers, :paths, :components,
17
75
  :security, :tags, :external_docs, :extension, :[], :each
18
76
 
19
- def initialize(input)
20
- @input = input
77
+ # @param [SourceInput] source_input
78
+ def initialize(source_input)
79
+ @reference_register = ReferenceRegister.new
80
+ @root_source = Source.new(source_input, self, reference_register)
81
+ @build_in_progress = false
82
+ @built = false
21
83
  end
22
84
 
85
+ # @return [Nodes::Openapi]
23
86
  def root
24
87
  factory.node
25
88
  end
26
89
 
27
- def resolve_reference(reference)
28
- if reference[0..1] != "#/"
29
- raise Error, "Only anchor references are currently supported"
30
- end
90
+ # All the additional sources that have been referenced as part of loading
91
+ # the OpenAPI document
92
+ #
93
+ # @return [Array<Source>]
94
+ def reference_sources
95
+ build unless built
96
+ reference_register.sources
97
+ end
31
98
 
32
- parts = reference.split("/").drop(1).map do |field|
33
- CGI.unescape(field.gsub("+", "%20"))
34
- end
99
+ # All of the sources involved in this OpenAPI document
100
+ #
101
+ # @return [Array<Source>]
102
+ def sources
103
+ [root_source] + reference_sources
104
+ end
35
105
 
36
- result = input.dig(*parts)
37
- raise Error, "Could not resolve reference #{reference}" unless result
106
+ # Any validation errors that are present on the OpenAPI document
107
+ #
108
+ # @return [Validation::ErrorCollection]
109
+ def errors
110
+ reference_factories.inject(factory.errors) do |memo, f|
111
+ Validation::ErrorCollection.combine(memo, f.errors)
112
+ end
113
+ end
38
114
 
39
- yield(result, parts)
115
+ # Look up whether an instance of SourceInput is already a known source
116
+ # for this document.
117
+ #
118
+ # @param [SourceInput] source_input
119
+ # @return [Source, nil]
120
+ def source_for_source_input(source_input)
121
+ sources.find { |source| source.source_input == source_input }
40
122
  end
41
123
 
42
124
  private
43
125
 
126
+ attr_reader :reference_register, :built, :build_in_progress
127
+
128
+ def build
129
+ return if build_in_progress || built
130
+ @build_in_progress = true
131
+ context = Context.root(root_source.data, root_source)
132
+ @factory = NodeFactories::Openapi.new(context)
133
+ reference_register.freeze
134
+ @build_in_progress = false
135
+ @built = true
136
+ end
137
+
44
138
  def factory
45
- @factory ||= NodeFactories::Openapi.new(Context.root(input, self))
139
+ build unless built
140
+ @factory
141
+ end
142
+
143
+ def reference_factories
144
+ build unless built
145
+ reference_register.factories
46
146
  end
47
147
  end
48
148
  end