brasilapi 0.7.0 → 0.8.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2d4feae01c7e83e53c8f30fe32b71bb335e64d17a05874b0e465abc695ba518a
4
- data.tar.gz: 8d36f60325e36d611da49f21a513fbe6e8551fea5f44446e7b9106c6d16ae221
3
+ metadata.gz: 67529435e017f1d14417130add2c21a99acc47126d6a0a15d8e7b308d3ede0e8
4
+ data.tar.gz: 3c83bc926f7873c8d973bb0433386b5ae937bca70b1ef5861322eab3e158a71b
5
5
  SHA512:
6
- metadata.gz: df32ff8fb00d74216638a81b683e1ca95abd42be4f0404deff5ada20ec1b52d411711a854aed8c4650e93b76d81ac587dab67db54c08abfa36907fbf9c987de8
7
- data.tar.gz: 005a4a9b91af86ad408f2f8fb5293d5285b2a974b4720d9b884c2e657f84286add533d0ed6a2d3cea75105c3ed4583ea89738fa44396b4e94430c421ba011b39
6
+ metadata.gz: 396e12a440bd9dfefd950016396c254c2816343a56ca5db4d34f8f433c8a99cae790f4620072270676cd128d774a36fd2adb7f76f6ccf851b92775a19d7b76b4
7
+ data.tar.gz: 721fe3a6d163b204cf5e065bd49c43116dc834ac8daa50a0e70909a038619f33cdfdf8770cfc1ae231f5c3ac3b7f4310a8bea3628b5099051b36e1080e12ccf2
data/.rubocop.yml CHANGED
@@ -1,8 +1,10 @@
1
- require: rubocop-rspec
1
+ plugins:
2
+ - rubocop-rspec
2
3
 
3
4
  AllCops:
4
5
  NewCops: enable
5
- TargetRubyVersion: 2.6
6
+ TargetRubyVersion: 3.3
7
+ SuggestExtensions: false
6
8
 
7
9
  Style/StringLiterals:
8
10
  Enabled: true
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.8.0] - 2026-10-20
4
+
5
+ - `BrasilAPI::NotFound` passa a herdar de `BrasilAPI::Error`, então `rescue BrasilAPI::Error` também captura 404
6
+ - Erros HTTP preservam status, corpo e os campos `message`/`type` da BrasilAPI; qualquer resposta 2xx é sucesso
7
+ - Novas subclasses `BadRequest` (400), `ServerError` (5xx) e `Timeout`, todas filhas de `BrasilAPI::Error`
8
+ - Timeout padrão de 10 segundos, configurável com `BrasilAPI.configure { |config| config.timeout = 30 }`
9
+ - Segmentos de URL são escapados com `URI.encode_uri_component`, sem `URI::Parser#escape` (obsoleto no Ruby 3.4+)
10
+ - FIPE deixa de enviar `tabela_referencia` vazio quando o parâmetro é nil
11
+ - CI dispara em push na `main` e em pull requests, nas Rubies suportadas 3.3, 3.4 e 4.0
12
+ - Ruby mínima sobe de 2.6 para 3.3, porque 2.6–3.2 estão fora de manutenção
13
+ - Publicação automática no RubyGems via Trusted Publishing (OIDC), sem API key
14
+ - Testes da API do CPTEC
15
+ - Metadado `github_repo` no gemspec
16
+ - Correção do texto "Dado aisso" na descrição do gemspec
17
+
3
18
  ## [0.7.0] - 2025-04-21
4
19
 
5
20
  - Integração da API do CPTEC
data/Gemfile CHANGED
@@ -7,8 +7,8 @@ gemspec
7
7
  group :development do
8
8
  gem "rake", "~> 13.0"
9
9
  gem "rspec", "~> 3.0"
10
- gem "rubocop", "~> 1.21"
11
- gem "rubocop-rspec", "~> 2.24"
10
+ gem "rubocop", "~> 1.86"
11
+ gem "rubocop-rspec", "~> 3.0"
12
12
  gem "vcr", "~> 6.3"
13
13
  gem "webmock", "~> 3.25"
14
14
  end
data/Gemfile.lock CHANGED
@@ -1,88 +1,87 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- brasilapi (0.7.0)
4
+ brasilapi (0.8.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
8
8
  specs:
9
- addressable (2.8.7)
10
- public_suffix (>= 2.0.2, < 7.0)
11
- ast (2.4.2)
12
- base64 (0.1.1)
13
- bigdecimal (3.1.9)
14
- crack (1.0.0)
9
+ addressable (2.9.0)
10
+ public_suffix (>= 2.0.2, < 8.0)
11
+ ast (2.4.3)
12
+ bigdecimal (4.1.3)
13
+ crack (1.0.1)
15
14
  bigdecimal
16
15
  rexml
17
- diff-lcs (1.5.0)
18
- hashdiff (1.1.2)
19
- json (2.6.3)
20
- language_server-protocol (3.17.0.3)
21
- parallel (1.23.0)
22
- parser (3.2.2.4)
16
+ diff-lcs (1.6.2)
17
+ hashdiff (1.2.1)
18
+ json (3.1.0)
19
+ language_server-protocol (3.17.0.6)
20
+ lint_roller (1.1.0)
21
+ parallel (2.3.0)
22
+ parser (3.3.12.0)
23
23
  ast (~> 2.4.1)
24
24
  racc
25
- public_suffix (6.0.1)
26
- racc (1.7.1)
25
+ prism (1.9.0)
26
+ public_suffix (7.0.5)
27
+ racc (1.8.1)
27
28
  rainbow (3.1.1)
28
- rake (13.0.6)
29
- regexp_parser (2.8.2)
30
- rexml (3.4.1)
31
- rspec (3.12.0)
32
- rspec-core (~> 3.12.0)
33
- rspec-expectations (~> 3.12.0)
34
- rspec-mocks (~> 3.12.0)
35
- rspec-core (3.12.2)
36
- rspec-support (~> 3.12.0)
37
- rspec-expectations (3.12.3)
29
+ rake (13.4.2)
30
+ regexp_parser (2.13.1)
31
+ rexml (3.4.4)
32
+ rspec (3.13.2)
33
+ rspec-core (~> 3.13.0)
34
+ rspec-expectations (~> 3.13.0)
35
+ rspec-mocks (~> 3.13.0)
36
+ rspec-core (3.13.6)
37
+ rspec-support (~> 3.13.0)
38
+ rspec-expectations (3.13.5)
38
39
  diff-lcs (>= 1.2.0, < 2.0)
39
- rspec-support (~> 3.12.0)
40
- rspec-mocks (3.12.6)
40
+ rspec-support (~> 3.13.0)
41
+ rspec-mocks (3.13.8)
41
42
  diff-lcs (>= 1.2.0, < 2.0)
42
- rspec-support (~> 3.12.0)
43
- rspec-support (3.12.1)
44
- rubocop (1.57.1)
45
- base64 (~> 0.1.1)
46
- json (~> 2.3)
47
- language_server-protocol (>= 3.17.0)
48
- parallel (~> 1.10)
49
- parser (>= 3.2.2.4)
43
+ rspec-support (~> 3.13.0)
44
+ rspec-support (3.13.7)
45
+ rubocop (1.92.0)
46
+ json (>= 2.3)
47
+ language_server-protocol (~> 3.17.0.2)
48
+ lint_roller (~> 1.1.0)
49
+ parallel (>= 1.10)
50
+ parser (>= 3.3.0.2)
50
51
  rainbow (>= 2.2.2, < 4.0)
51
- regexp_parser (>= 1.8, < 3.0)
52
- rexml (>= 3.2.5, < 4.0)
53
- rubocop-ast (>= 1.28.1, < 2.0)
52
+ regexp_parser (>= 2.11.3, < 3.0)
53
+ rubocop-ast (>= 1.49.0, < 2.0)
54
54
  ruby-progressbar (~> 1.7)
55
- unicode-display_width (>= 2.4.0, < 3.0)
56
- rubocop-ast (1.29.0)
57
- parser (>= 3.2.1.0)
58
- rubocop-capybara (2.19.0)
59
- rubocop (~> 1.41)
60
- rubocop-factory_bot (2.24.0)
61
- rubocop (~> 1.33)
62
- rubocop-rspec (2.24.1)
63
- rubocop (~> 1.33)
64
- rubocop-capybara (~> 2.17)
65
- rubocop-factory_bot (~> 2.22)
55
+ unicode-display_width (>= 2.4.0, < 4.0)
56
+ rubocop-ast (1.50.0)
57
+ parser (>= 3.3.7.2)
58
+ prism (~> 1.7)
59
+ rubocop-rspec (3.10.2)
60
+ lint_roller (~> 1.1)
61
+ regexp_parser (>= 2.0)
62
+ rubocop (~> 1.86, >= 1.86.2)
66
63
  ruby-progressbar (1.13.0)
67
- unicode-display_width (2.5.0)
68
- vcr (6.3.1)
69
- base64
70
- webmock (3.25.1)
64
+ unicode-display_width (3.3.0)
65
+ unicode-emoji (~> 4.3)
66
+ unicode-emoji (4.3.0)
67
+ vcr (6.4.0)
68
+ webmock (3.34.0)
71
69
  addressable (>= 2.8.0)
72
70
  crack (>= 0.3.2)
73
71
  hashdiff (>= 0.4.0, < 2.0.0)
74
72
 
75
73
  PLATFORMS
74
+ ruby
76
75
  x86_64-linux
77
76
 
78
77
  DEPENDENCIES
79
78
  brasilapi!
80
79
  rake (~> 13.0)
81
80
  rspec (~> 3.0)
82
- rubocop (~> 1.21)
83
- rubocop-rspec (~> 2.24)
81
+ rubocop (~> 1.86)
82
+ rubocop-rspec (~> 3.0)
84
83
  vcr (~> 6.3)
85
84
  webmock (~> 3.25)
86
85
 
87
86
  BUNDLED WITH
88
- 2.4.10
87
+ 4.0.23
data/README.md CHANGED
@@ -138,6 +138,30 @@ BrasilAPI::IBGE.cities_by_state('CE')
138
138
  ```
139
139
 
140
140
 
141
+ ## Configuration
142
+
143
+ Requests time out after 10 seconds (`open_timeout` and `read_timeout`). Override the default when a provider is slow:
144
+
145
+ ```ruby
146
+ BrasilAPI.configure do |config|
147
+ config.timeout = 30
148
+ end
149
+ ```
150
+
151
+ `Net::OpenTimeout` and `Net::ReadTimeout` are raised as `BrasilAPI::Timeout`. HTTP failures are `BrasilAPI::Error` (or a subclass such as `NotFound`, `BadRequest` and `ServerError`) and expose `status`, `message`, `type` and `body`.
152
+
153
+ ## Release
154
+
155
+ Publishing to RubyGems happens when a pull request that bumps the version is merged into `main`:
156
+
157
+ 1. Bump `BrasilAPI::VERSION` in `lib/brasil_api/version.rb`.
158
+ 2. Move the `[Unreleased]` section of `CHANGELOG.md` into that version, with the date.
159
+ 3. Open a pull request and merge it into `main`.
160
+
161
+ `.github/workflows/release.yml` runs the test suite, creates the `vX.Y.Z` tag, publishes a GitHub Release using that version's changelog notes, and pushes the gem with RubyGems Trusted Publishing (OIDC). If the version already has a `vX.Y.Z` tag, the workflow does nothing.
162
+
163
+ Before the first automated publish, register a trusted publisher on rubygems.org for `dayvidemerson/brasilapi-rb` and the workflow file `release.yml`, with no GitHub environment. The gemspec keeps `rubygems_mfa_required`.
164
+
141
165
  # License
142
166
  [MIT](./LICENSE)
143
167
 
@@ -2,44 +2,122 @@
2
2
 
3
3
  require "json"
4
4
  require "net/http"
5
+ require "uri"
5
6
 
6
7
  module BrasilAPI
7
8
  class Base
8
9
  BASE_URL = "https://brasilapi.com.br/api"
10
+
9
11
  class << self
10
12
  def get(path, params = {})
11
- uri = URI("#{BASE_URL}#{path}")
12
- uri.query = URI.encode_www_form(params) if params.any?
13
-
14
- request_data(uri)
13
+ request_data(build_uri(path, params))
15
14
  end
16
15
 
17
16
  protected
18
17
 
19
18
  def request_data(uri)
20
- response = Net::HTTP.get_response(uri)
19
+ response = perform_request(uri)
20
+ return parsed_body(response) if response.is_a?(Net::HTTPSuccess)
21
21
 
22
- case status_for(response)
23
- when 200
24
- parsed_body(response)
25
- when 404
26
- raise BrasilAPI::NotFound
27
- else
28
- raise BrasilAPI::Error
29
- end
22
+ raise_api_error(response)
23
+ rescue Net::OpenTimeout, Net::ReadTimeout => e
24
+ raise BrasilAPI::Timeout, e.message
25
+ end
26
+
27
+ private
28
+
29
+ def build_uri(path, params)
30
+ uri = URI.parse("#{BASE_URL}#{encode_path(path)}")
31
+ query = params.compact
32
+ uri.query = URI.encode_www_form(query) unless query.empty?
33
+ uri
34
+ end
35
+
36
+ def encode_path(path)
37
+ path.split("/", -1).map { |segment| encode_segment(segment) }.join("/")
30
38
  end
31
39
 
32
- def status_for(response)
33
- status = {
34
- Net::HTTPOK => 200,
35
- Net::HTTPNotFound => 404
36
- }
40
+ # Encode each segment on its own so "?", "#" and spaces stay inside the path.
41
+ # encode_uri_component keeps spaces as %20, matching the recorded cassettes.
42
+ def encode_segment(segment)
43
+ URI.encode_uri_component(segment)
44
+ end
37
45
 
38
- status[response.class]
46
+ def perform_request(uri)
47
+ Net::HTTP.start(
48
+ uri.hostname,
49
+ uri.port,
50
+ use_ssl: uri.scheme == "https",
51
+ open_timeout: BrasilAPI.config.timeout,
52
+ read_timeout: BrasilAPI.config.timeout
53
+ ) do |http|
54
+ http.request(Net::HTTP::Get.new(uri))
55
+ end
39
56
  end
40
57
 
41
58
  def parsed_body(response)
42
- JSON.parse(response.body) if response.body
59
+ body = utf8_body(response)
60
+ return if body.nil? || body.empty?
61
+
62
+ JSON.parse(body)
63
+ end
64
+
65
+ def raise_api_error(response)
66
+ parsed = parse_error_body(response)
67
+ raise error_class_for(response).new(
68
+ error_message(parsed, response),
69
+ status: response.code.to_i,
70
+ body: error_body(parsed, response),
71
+ type: error_type(parsed)
72
+ )
73
+ end
74
+
75
+ def parse_error_body(response)
76
+ body = utf8_body(response)
77
+ return if body.nil? || body.empty?
78
+
79
+ JSON.parse(body)
80
+ rescue JSON::ParserError
81
+ nil
82
+ end
83
+
84
+ def utf8_body(response)
85
+ body = response.body
86
+ return if body.nil?
87
+
88
+ body.dup.force_encoding(Encoding::UTF_8)
89
+ end
90
+
91
+ def error_message(parsed, response)
92
+ return parsed["message"] if parsed.is_a?(Hash) && parsed["message"]
93
+
94
+ body = utf8_body(response)
95
+ return body if body && !body.empty?
96
+
97
+ "HTTP #{response.code}"
98
+ end
99
+
100
+ def error_body(parsed, response)
101
+ return parsed unless parsed.nil?
102
+
103
+ utf8_body(response)
104
+ end
105
+
106
+ def error_type(parsed)
107
+ parsed["type"] if parsed.is_a?(Hash)
108
+ end
109
+
110
+ def error_class_for(response)
111
+ case response.code.to_i
112
+ when 400
113
+ BrasilAPI::BadRequest
114
+ when 404
115
+ BrasilAPI::NotFound
116
+ when 500..599
117
+ BrasilAPI::ServerError
118
+ else
119
+ BrasilAPI::Error
120
+ end
43
121
  end
44
122
  end
45
123
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrasilAPI
4
+ class Configuration
5
+ DEFAULT_TIMEOUT = 10
6
+
7
+ attr_accessor :timeout
8
+
9
+ def initialize
10
+ @timeout = DEFAULT_TIMEOUT
11
+ end
12
+ end
13
+
14
+ class << self
15
+ def configure
16
+ yield config
17
+ end
18
+
19
+ def config
20
+ @config ||= Configuration.new
21
+ end
22
+
23
+ def reset_config!
24
+ @config = Configuration.new
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrasilAPI
4
+ class Error < StandardError
5
+ attr_reader :status, :body, :type
6
+
7
+ def initialize(message = nil, status: nil, body: nil, type: nil)
8
+ @status = status
9
+ @body = body
10
+ @type = type
11
+ super(message)
12
+ end
13
+ end
14
+
15
+ class NotFound < Error; end
16
+
17
+ class BadRequest < Error; end
18
+
19
+ class ServerError < Error; end
20
+
21
+ class Timeout < Error; end
22
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BrasilAPI
4
- VERSION = "0.7.0"
4
+ VERSION = "0.8.0"
5
5
  end
data/lib/brasil_api.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "brasil_api/version"
4
+ require_relative "brasil_api/configuration"
5
+ require_relative "brasil_api/error"
4
6
  require_relative "brasil_api/base"
5
7
  require_relative "brasil_api/address"
6
8
  require_relative "brasil_api/bank"
@@ -15,8 +17,3 @@ require_relative "brasil_api/ncm"
15
17
  require_relative "brasil_api/pix"
16
18
  require_relative "brasil_api/registrobr"
17
19
  require_relative "brasil_api/taxas"
18
-
19
- module BrasilAPI
20
- class Error < StandardError; end
21
- class NotFound < StandardError; end
22
- end
metadata CHANGED
@@ -1,19 +1,19 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brasilapi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dayvid Emerson
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-04-21 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies: []
12
12
  description: Acesso programático de informações é algo fundamental na comunicação
13
13
  entre sistemas, mas, para nossa surpresa, uma informação tão útil e pública quanto
14
14
  um CEP não consegue ser acessada diretamente por um navegador por conta da API dos
15
- Correios não possuir CORS habilitado. Dado aisso, este projeto experimental tem
16
- como objetivo centralizar e disponibilizar endpoints modernos com baixíssima latência
15
+ Correios não possuir CORS habilitado. Dado isso, este projeto experimental tem como
16
+ objetivo centralizar e disponibilizar endpoints modernos com baixíssima latência
17
17
  utilizando tecnologias como Vercel Smart CDN responsável por fazer o cache das informações
18
18
  em atualmente 23 regiões distribuídas ao longo do mundo (incluindo Brasil). Então
19
19
  não importa o quão devagar for a fonte dos dados, nós queremos disponibilizá-la
@@ -33,14 +33,15 @@ files:
33
33
  - LICENSE.txt
34
34
  - README.md
35
35
  - Rakefile
36
- - brasilapi.gemspec
37
36
  - lib/brasil_api.rb
38
37
  - lib/brasil_api/address.rb
39
38
  - lib/brasil_api/bank.rb
40
39
  - lib/brasil_api/base.rb
41
40
  - lib/brasil_api/company.rb
41
+ - lib/brasil_api/configuration.rb
42
42
  - lib/brasil_api/cptec.rb
43
43
  - lib/brasil_api/cvm.rb
44
+ - lib/brasil_api/error.rb
44
45
  - lib/brasil_api/fipe.rb
45
46
  - lib/brasil_api/holiday.rb
46
47
  - lib/brasil_api/ibge.rb
@@ -59,6 +60,7 @@ metadata:
59
60
  source_code_uri: https://github.com/dayvidemerson/brasilapi-rb
60
61
  changelog_uri: https://github.com/dayvidemerson/brasilapi-rb/blob/main/CHANGELOG.md
61
62
  rubygems_mfa_required: 'true'
63
+ github_repo: ssh://github.com/dayvidemerson/brasilapi-rb
62
64
  rdoc_options: []
63
65
  require_paths:
64
66
  - lib
@@ -66,14 +68,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
66
68
  requirements:
67
69
  - - ">="
68
70
  - !ruby/object:Gem::Version
69
- version: 2.6.0
71
+ version: 3.3.0
70
72
  required_rubygems_version: !ruby/object:Gem::Requirement
71
73
  requirements:
72
74
  - - ">="
73
75
  - !ruby/object:Gem::Version
74
76
  version: '0'
75
77
  requirements: []
76
- rubygems_version: 3.6.2
78
+ rubygems_version: 3.6.9
77
79
  specification_version: 4
78
80
  summary: Cliente desenvolvido em Ruby para o consumo da Brasil API que permite buscar
79
81
  informações públicas referentes ao Brasil de modo eficiente e simplificado.
data/brasilapi.gemspec DELETED
@@ -1,42 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "lib/brasil_api/version"
4
-
5
- Gem::Specification.new do |spec|
6
- spec.name = "brasilapi"
7
- spec.version = BrasilAPI::VERSION
8
- spec.authors = ["Dayvid Emerson"]
9
- spec.email = ["dayvidemerson96@gmail.com"]
10
-
11
- spec.summary = "Cliente desenvolvido em Ruby para o consumo da Brasil API que permite buscar informações públicas " \
12
- "referentes ao Brasil de modo eficiente e simplificado."
13
-
14
- spec.description = "Acesso programático de informações é algo fundamental na comunicação entre sistemas, mas, para " \
15
- "nossa surpresa, uma informação tão útil e pública quanto um CEP não consegue ser acessada " \
16
- "diretamente por um navegador por conta da API dos Correios não possuir CORS habilitado. Dado a" \
17
- "isso, este projeto experimental tem como objetivo centralizar e disponibilizar endpoints " \
18
- "modernos com baixíssima latência utilizando tecnologias como Vercel Smart CDN responsável por " \
19
- "fazer o cache das informações em atualmente 23 regiões distribuídas ao longo do mundo " \
20
- "(incluindo Brasil). Então não importa o quão devagar for a fonte dos dados, nós queremos " \
21
- "disponibilizá-la da forma mais rápida e moderna possível."
22
-
23
- spec.homepage = "https://brasilapi.com.br"
24
- spec.license = "MIT"
25
- spec.required_ruby_version = ">= 2.6.0"
26
-
27
- spec.metadata["homepage_uri"] = spec.homepage
28
- spec.metadata["source_code_uri"] = "https://github.com/dayvidemerson/brasilapi-rb"
29
- spec.metadata["changelog_uri"] = "https://github.com/dayvidemerson/brasilapi-rb/blob/main/CHANGELOG.md"
30
- spec.metadata["rubygems_mfa_required"] = "true"
31
-
32
- # Specify which files should be added to the gem when it is released.
33
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
34
- spec.files = Dir.chdir(__dir__) do
35
- `git ls-files -z`.split("\x0").reject do |f|
36
- (File.expand_path(f) == __FILE__) || f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor])
37
- end
38
- end
39
- spec.bindir = "exe"
40
- spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
41
- spec.require_paths = ["lib"]
42
- end