openapi3_parser 0.2.0 → 0.3.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 (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
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openapi3_parser/error"
4
+
5
+ module Openapi3Parser
6
+ class Document
7
+ class ReferenceRegister
8
+ attr_reader :sources, :factories
9
+
10
+ def initialize
11
+ @sources = []
12
+ @factories = []
13
+ end
14
+
15
+ def register(factory)
16
+ error = "Can't register references when the register is frozen"
17
+ raise Error::ImmutableObject, error if frozen?
18
+ context = factory.context
19
+ add_source(context.source_location.source)
20
+ add_factory(factory)
21
+ end
22
+
23
+ def freeze
24
+ sources.freeze
25
+ factories.freeze
26
+ super
27
+ end
28
+
29
+ private
30
+
31
+ def add_source(source)
32
+ return if sources.include?(source)
33
+ sources << source
34
+ end
35
+
36
+ def add_factory(factory)
37
+ return if factory_registered?(factory)
38
+ factories << factory
39
+ end
40
+
41
+ def factory_registered?(factory)
42
+ source_location = factory.context.source_location
43
+ factories.any? do |f|
44
+ same_location = f.context.source_location == source_location
45
+ same_location && f.class == factory.class
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -1,5 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Openapi3Parser
4
- class Error < ::RuntimeError; end
4
+ # An abstract class for Exceptions produced by this Gem
5
+ class Error < ::RuntimeError
6
+ # Raised in cases where we have been provided a path or URL to a file and
7
+ # at runtime when we have tried to access that resource it is not available
8
+ # for whatever reason.
9
+ class InaccessibleInput < Error; end
10
+ # Raised in cases where we provided data that we expected to be parsable
11
+ # (such as a string of JSON data) but when we tried to parse it an error
12
+ # is raised
13
+ class UnparsableInput < Error; end
14
+ # Raised in cases where an object that is in an immutable state is modified
15
+ #
16
+ # Typically this would occur when a component that is frozen is modififed.
17
+ # Some components are mutable during the construction of a document and
18
+ # then frozen afterwards.
19
+ class ImmutableObject < Error; end
20
+ # Raised when a type that is not a whitelist of valid types is used
21
+ class InvalidType < Error; end
22
+ # Raised when we have to abort creating an object due to invalid data
23
+ class InvalidData < Error; end
24
+ # Used when there are fields that are missing from an object which prevents
25
+ # us from creating a node
26
+ class MissingFields < Error; end
27
+ # Used when there are extra fields that are not expected in the data for
28
+ # a node
29
+ class UnexpectedFields < Error; end
30
+ end
5
31
  end
@@ -12,14 +12,39 @@ module Openapi3Parser
12
12
  @node_context = context
13
13
  end
14
14
 
15
+ # Look up an attribute of the node by the name it has in the OpenAPI
16
+ # document.
17
+ #
18
+ # @example Look up by OpenAPI naming
19
+ # obj["externalDocs"]
20
+ #
21
+ # @example Look up by symbol
22
+ # obj[:servers]
23
+ #
24
+ # @example Look up an extension
25
+ # obj["x-myExtension"]
26
+ #
27
+ # @param [String, Symbol] value
28
+ #
29
+ # @return anything
15
30
  def [](value)
16
- node_data[value]
31
+ node_data[value.to_s]
17
32
  end
18
33
 
34
+ # Look up an extension provided for this object, doesn't need a prefix of
35
+ # "x-"
36
+ #
37
+ # @example Looking up an extension provided as "x-extra"
38
+ # obj.extension("extra")
39
+ #
40
+ # @param [String, Symbol] value
41
+ #
42
+ # @return [Hash, Array, Numeric, String, true, false, nil]
19
43
  def extension(value)
20
44
  node_data["x-#{value}"]
21
45
  end
22
46
 
47
+ # Iterate through the attributes of this object
23
48
  def each(&block)
24
49
  node_data.each(&block)
25
50
  end
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "openapi3_parser/context"
4
+ require "openapi3_parser/error"
3
5
  require "openapi3_parser/node_factory"
4
6
  require "openapi3_parser/nodes/array"
5
7
  require "openapi3_parser/validation/error"
8
+ require "openapi3_parser/validation/error_collection"
6
9
 
7
10
  module Openapi3Parser
8
11
  module NodeFactories
@@ -17,11 +20,11 @@ module Openapi3Parser
17
20
  value_factory: nil,
18
21
  validate: nil
19
22
  )
20
- super(context)
21
23
  @default = default
22
24
  @given_value_input_type = value_input_type
23
25
  @given_value_factory = value_factory
24
26
  @given_validate = validate
27
+ super(context)
25
28
  end
26
29
 
27
30
  private
@@ -32,7 +35,7 @@ module Openapi3Parser
32
35
  def process_input(input)
33
36
  input.each_with_index.map do |value, i|
34
37
  if value_factory?
35
- initialize_value_factory(context.next_namespace(i))
38
+ initialize_value_factory(Context.next_field(context, i))
36
39
  else
37
40
  value
38
41
  end
@@ -43,15 +46,13 @@ module Openapi3Parser
43
46
  given_validate&.call(input, context)
44
47
  end
45
48
 
46
- def validate_input(error_collection)
47
- super(error_collection)
48
- processed_input.each do |value|
49
- next unless value.respond_to?(:errors)
50
- error_collection.merge(value.errors)
49
+ def validate_input
50
+ value_errors = processed_input.flat_map do |value|
51
+ value.respond_to?(:errors) ? value.errors.to_a : []
51
52
  end
52
- error_collection.merge(
53
- validate_value_input_type(processed_input, context)
54
- )
53
+ errors = value_errors + validate_value_input_type(processed_input,
54
+ context)
55
+ Validation::ErrorCollection.combine(super, errors)
55
56
  end
56
57
 
57
58
  def build_node(input)
@@ -81,7 +82,7 @@ module Openapi3Parser
81
82
  error = error_for_value_input_type(value)
82
83
  next unless error
83
84
  memo << Validation::Error.new(
84
- context.next_namespace(i), error
85
+ error, Context.next_field(context, i), self.class
85
86
  )
86
87
  end
87
88
  end
@@ -90,9 +91,9 @@ module Openapi3Parser
90
91
  input.each_with_index do |value, i|
91
92
  error = error_for_value_input_type(value)
92
93
  next unless error
93
- next_context = context.next_namespace(i)
94
- raise Openapi3Parser::Error,
95
- "Invalid type for #{next_context.stringify_namespace}. "\
94
+ next_context = Context.next_field(context, i)
95
+ raise Openapi3Parser::Error::InvalidType,
96
+ "Invalid type for #{next_context.location_summary}. "\
96
97
  "#{error}"
97
98
  end
98
99
  end
@@ -14,7 +14,8 @@ module Openapi3Parser
14
14
  def process_input(input)
15
15
  input.each_with_object({}) do |(key, value), memo|
16
16
  memo[key] = value if extension?(key)
17
- memo[key] = NodeFactories::PathItem.new(context.next_namespace(key))
17
+ next_context = Context.next_field(context, key)
18
+ memo[key] = NodeFactories::PathItem.new(next_context)
18
19
  end
19
20
  end
20
21
 
@@ -12,7 +12,7 @@ module Openapi3Parser
12
12
 
13
13
  allow_extensions
14
14
 
15
- # @TODO The link object in OAS is pretty meaty and there's lot of scope
15
+ # @todo The link object in OAS is pretty meaty and there's lot of scope
16
16
  # for further work here to make use of it's funcationality
17
17
 
18
18
  field "operationRef", input_type: String
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "openapi3_parser/context"
4
+ require "openapi3_parser/error"
3
5
  require "openapi3_parser/node_factory/map"
4
6
  require "openapi3_parser/nodes/map"
5
7
  require "openapi3_parser/validation/error"
8
+ require "openapi3_parser/validation/error_collection"
6
9
 
7
10
  module Openapi3Parser
8
11
  module NodeFactories
@@ -16,11 +19,11 @@ module Openapi3Parser
16
19
  value_factory: nil,
17
20
  validate: nil
18
21
  )
19
- super(context)
20
22
  @given_key_input_type = key_input_type
21
23
  @given_value_input_type = value_input_type
22
24
  @given_value_factory = value_factory
23
25
  @given_validate = validate
26
+ super(context)
24
27
  end
25
28
 
26
29
  private
@@ -31,7 +34,8 @@ module Openapi3Parser
31
34
  def process_input(input)
32
35
  input.each_with_object({}) do |(key, value), memo|
33
36
  memo[key] = if value_factory?
34
- initialize_value_factory(context.next_namespace(key))
37
+ next_context = Context.next_field(context, key)
38
+ initialize_value_factory(next_context)
35
39
  else
36
40
  value
37
41
  end
@@ -53,11 +57,9 @@ module Openapi3Parser
53
57
  given_validate&.call(input, context)
54
58
  end
55
59
 
56
- def validate_input(error_collection)
57
- super(error_collection)
58
- error_collection.merge(
59
- validate_value_input_type(processed_input, context)
60
- )
60
+ def validate_input
61
+ errors = validate_value_input_type(processed_input, context)
62
+ Validation::ErrorCollection.combine(super, errors)
61
63
  end
62
64
 
63
65
  def build_node(input)
@@ -84,7 +86,7 @@ module Openapi3Parser
84
86
  error = error_for_value_input_type(value)
85
87
  next unless error
86
88
  memo << Validation::Error.new(
87
- context.next_namespace(key), error
89
+ error, Context.next_field(context, key), self.class
88
90
  )
89
91
  end
90
92
  end
@@ -93,9 +95,9 @@ module Openapi3Parser
93
95
  input.each do |key, value|
94
96
  error = error_for_value_input_type(value)
95
97
  next unless error
96
- next_context = context.next_namespace(key)
97
- raise Openapi3Parser::Error,
98
- "Invalid type for #{next_context.stringify_namespace}. "\
98
+ next_context = Context.next_field(context, key)
99
+ raise Openapi3Parser::Error::InvalidType,
100
+ "Invalid type for #{next_context.location_summary}. "\
99
101
  "#{error}"
100
102
  end
101
103
  end
@@ -17,6 +17,8 @@ module Openapi3Parser
17
17
  include NodeFactory::Object
18
18
 
19
19
  allow_extensions
20
+ disallow_default
21
+
20
22
  field "openapi", input_type: String, required: true
21
23
  field "info", factory: NodeFactories::Info, required: true
22
24
  field "servers", factory: :servers_factory
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "openapi3_parser/nodes/path_item"
4
+ require "openapi3_parser/node_factory/fields/reference"
4
5
  require "openapi3_parser/node_factory/object"
5
6
  require "openapi3_parser/node_factory/object/node_builder"
6
7
  require "openapi3_parser/node_factory/optional_reference"
@@ -15,7 +16,7 @@ module Openapi3Parser
15
16
  include NodeFactory::Object
16
17
 
17
18
  allow_extensions
18
- field "$ref", input_type: String
19
+ field "$ref", input_type: String, factory: :ref_factory
19
20
  field "summary", input_type: String
20
21
  field "description", input_type: String
21
22
  field "get", factory: :operation_factory
@@ -36,9 +37,17 @@ module Openapi3Parser
36
37
  private
37
38
 
38
39
  def build_object(data, context)
39
- merged_data = merge_reference(data, context)
40
- merged_data.delete("$ref")
41
- Nodes::PathItem.new(merged_data, context)
40
+ ref = data.delete("$ref")
41
+ return Nodes::PathItem.new(data, context) unless ref
42
+
43
+ merged_data = ref.node_data.merge(data) do |_, new, old|
44
+ new.nil? ? old : new
45
+ end
46
+ Nodes::PathItem.new(merged_data, ref.node_context)
47
+ end
48
+
49
+ def ref_factory(context)
50
+ Fields::Reference.new(context, self.class)
42
51
  end
43
52
 
44
53
  def operation_factory(context)
@@ -56,20 +65,6 @@ module Openapi3Parser
56
65
  factory = NodeFactory::OptionalReference.new(NodeFactories::Parameter)
57
66
  NodeFactories::Array.new(context, value_factory: factory)
58
67
  end
59
-
60
- def merge_reference(data, context)
61
- return data unless data["$ref"]
62
- factory = context.resolve_reference do |ref_context|
63
- self.class.new(ref_context)
64
- end
65
-
66
- # @TODO In this situation we're basically sacrificing the reference
67
- # context as we don't know how to merge them. Should develop a system
68
- # to have a dual context
69
- factory.node_data.merge(data) do |_, new, old|
70
- new.nil? ? old : new
71
- end
72
- end
73
68
  end
74
69
  end
75
70
  end
@@ -15,7 +15,8 @@ module Openapi3Parser
15
15
  def process_input(input)
16
16
  input.each_with_object({}) do |(key, value), memo|
17
17
  memo[key] = value if extension?(key)
18
- memo[key] = child_factory(context.next_namespace(key))
18
+ next_context = Context.next_field(context, key)
19
+ memo[key] = child_factory(next_context)
19
20
  end
20
21
  end
21
22
 
@@ -1,32 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "openapi3_parser/node_factory/object"
4
+ require "openapi3_parser/node_factory/fields/reference"
4
5
 
5
6
  module Openapi3Parser
6
7
  module NodeFactories
7
8
  class Reference
8
9
  include NodeFactory::Object
9
10
 
10
- field "$ref", input_type: String, required: true
11
+ field "$ref", input_type: String, required: true, factory: :ref_factory
11
12
 
12
13
  attr_reader :factory
13
14
 
14
15
  def initialize(context, factory)
15
- super(context)
16
16
  @factory = factory
17
+ super(context)
17
18
  end
18
19
 
19
20
  private
20
21
 
21
- def build_object(_, context)
22
- context.resolve_reference do |ref_context|
23
- resolve_factory(ref_context).node
24
- end
22
+ def build_node(input)
23
+ input["$ref"].node
25
24
  end
26
25
 
27
- def resolve_factory(ref_context)
28
- return factory.new(ref_context) if factory.is_a?(Class)
29
- factory.call(ref_context)
26
+ def ref_factory(context)
27
+ Fields::Reference.new(context, factory)
30
28
  end
31
29
  end
32
30
  end
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "openapi3_parser/context"
4
+ require "openapi3_parser/node_factories/response"
3
5
  require "openapi3_parser/node_factory/map"
4
6
  require "openapi3_parser/node_factory/optional_reference"
5
7
  require "openapi3_parser/nodes/responses"
6
- require "openapi3_parser/node_factories/response"
7
8
 
8
9
  module Openapi3Parser
9
10
  module NodeFactories
@@ -15,7 +16,8 @@ module Openapi3Parser
15
16
  def process_input(input)
16
17
  input.each_with_object({}) do |(key, value), memo|
17
18
  memo[key] = value if extension?(key)
18
- memo[key] = child_factory(context.next_namespace(key))
19
+ next_context = Context.next_field(context, key)
20
+ memo[key] = child_factory(next_context)
19
21
  end
20
22
  end
21
23
 
@@ -13,7 +13,8 @@ module Openapi3Parser
13
13
 
14
14
  def process_input(input)
15
15
  input.keys.each_with_object({}) do |key, memo|
16
- memo[key] = NodeFactories::Array.new(context.next_namespace(key))
16
+ next_context = Context.next_field(context, key)
17
+ memo[key] = NodeFactories::Array.new(next_context)
17
18
  end
18
19
  end
19
20
 
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "openapi3_parser/context"
3
4
  require "openapi3_parser/error"
4
5
  require "openapi3_parser/validation/error"
5
6
  require "openapi3_parser/validation/error_collection"
@@ -19,18 +20,36 @@ module Openapi3Parser
19
20
  def expected_input_type
20
21
  @input_type
21
22
  end
23
+
24
+ def allow_default
25
+ @allow_default = true
26
+ end
27
+
28
+ def disallow_default
29
+ @allow_default = false
30
+ end
31
+
32
+ def allowed_default?
33
+ @allow_default.nil? || @allow_default
34
+ end
22
35
  end
23
36
 
24
37
  def self.included(base)
25
38
  base.extend(ClassMethods)
26
39
  end
27
40
 
41
+ def allowed_default?
42
+ self.class.allowed_default?
43
+ end
44
+
28
45
  EXTENSION_REGEX = /^x-(.*)/
29
46
 
30
47
  attr_reader :context
31
48
 
32
49
  def initialize(context)
33
50
  @context = context
51
+ input = nil_input? ? default : context.input
52
+ @processed_input = input.nil? ? nil : process_input(input)
34
53
  end
35
54
 
36
55
  def valid?
@@ -51,47 +70,44 @@ module Openapi3Parser
51
70
 
52
71
  private
53
72
 
73
+ attr_reader :processed_input
74
+
54
75
  def validate(_input, _context); end
55
76
 
56
- def validate_input(error_collection)
57
- add_validation_errors(
58
- validate(context.input, context),
59
- error_collection
60
- )
77
+ def validate_input
78
+ transform_errors(validate(context.input, context))
61
79
  end
62
80
 
63
- def add_validation_errors(errors, error_collection)
64
- errors = Array(errors)
65
- errors.each do |error|
66
- unless error.is_a?(Validation::Error)
67
- error = Validation::Error.new(context.namespace, error)
81
+ def transform_errors(errors)
82
+ error_objects = Array(errors).map do |error|
83
+ if !error.is_a?(Validation::Error)
84
+ error
85
+ else
86
+ Validation::Error.new(error, context, self.class)
68
87
  end
69
- error_collection.append(error)
70
88
  end
71
- error_collection
89
+ Validation::ErrorCollection.new(error_objects)
72
90
  end
73
91
 
74
92
  def build_errors
75
- error_collection = Validation::ErrorCollection.new
76
- return error_collection if nil_input?
93
+ return Validation::ErrorCollection.new if nil_input? && allowed_default?
77
94
  unless valid_type?
78
95
  error = Validation::Error.new(
79
- context.namespace, "Invalid type. #{validate_type}"
96
+ "Invalid type. #{validate_type}", context, self.class
80
97
  )
81
- return error_collection.tap { |ec| ec.append(error) }
98
+ return Validation::ErrorCollection.new([error])
82
99
  end
83
- validate_input(error_collection)
84
- error_collection
100
+ validate_input
85
101
  end
86
102
 
87
103
  def build_valid_node
88
- if nil_input?
104
+ if nil_input? && allowed_default?
89
105
  return default.nil? ? nil : build_node(processed_input)
90
106
  end
91
107
 
92
108
  unless valid_type?
93
- raise Openapi3Parser::Error,
94
- "Invalid type for #{context.stringify_namespace}. "\
109
+ raise Openapi3Parser::Error::InvalidType,
110
+ "Invalid type for #{context.location_summary}. "\
95
111
  "#{validate_type}"
96
112
  end
97
113
 
@@ -102,8 +118,8 @@ module Openapi3Parser
102
118
  def validate_before_build
103
119
  errors = Array(validate(context.input, context))
104
120
  return unless errors.any?
105
- raise Openapi3Parser::Error,
106
- "Invalid data for #{context.stringify_namespace}. "\
121
+ raise Openapi3Parser::Error::InvalidData,
122
+ "Invalid data for #{context.location_summary}. "\
107
123
  "#{errors.join(', ')}"
108
124
  end
109
125
 
@@ -116,13 +132,6 @@ module Openapi3Parser
116
132
  return "Expected #{self.class.expected_input_type}" unless valid_type
117
133
  end
118
134
 
119
- def processed_input
120
- @processed_input ||= begin
121
- input = nil_input? ? default : context.input
122
- process_input(input)
123
- end
124
- end
125
-
126
135
  def process_input(input)
127
136
  input
128
137
  end