concurso_hub 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8c7322f931071bf6109d00a6359faab176c639da4073bf3147807c0a04860262
4
+ data.tar.gz: bb8af9915024a3bb6134f401dd1d60e8a5b5256e3abe54888d24e20375001f12
5
+ SHA512:
6
+ metadata.gz: '0294a2295e608584c7c92b425d809b53e61acd1c6a752e98a69d0027590a1198c8183d3e772a0e9ec186b92c752a752a090cd9d3fa89c9a09f5c9f298bfb469b'
7
+ data.tar.gz: 37d8ea5a7e53692a5cedbc905abc0a6157709da4d6d16dac3f55d3f26b9d1648b8fb12918b874ede0f3580c99cf37b5091ba0036afb537c2a9eb6806281f8005
data/README.md ADDED
@@ -0,0 +1,511 @@
1
+ # ConcursoHub
2
+
3
+ Gem Ruby para busca e extração de dados de concursos públicos brasileiros a partir do [PCI Concursos](https://pciconcursos.com.br).
4
+
5
+ Projetada com **arquitetura hexagonal (Ports & Adapters)**, a biblioteca mantém o núcleo de negócio completamente desacoplado de I/O — tornando-a ideal para uso em **APIs backend** (Rails, Sinatra, Grape, etc.) que precisam expor dados de concursos para um frontend consumir.
6
+
7
+ ---
8
+
9
+ ## Sumário
10
+
11
+ - [Instalação](#instalação)
12
+ - [Uso via CLI](#uso-via-cli)
13
+ - [Uso como gem em uma API backend](#uso-como-gem-em-uma-api-backend)
14
+ - [ConcursoHub.search](#concursoscrapersearch)
15
+ - [ConcursoHub.edital](#concursoscraperedital)
16
+ - [ConcursoHub.provas](#concursoscraperprovas)
17
+ - [Exemplo completo em Rails](#exemplo-completo-em-rails)
18
+ - [Uso avançado (injeção de dependências)](#uso-avançado-injeção-de-dependências)
19
+ - [Listar concursos](#listar-concursos)
20
+ - [Ver edital completo](#ver-edital-completo)
21
+ - [Baixar PDFs de um edital](#baixar-pdfs-de-um-edital)
22
+ - [Baixar provas e gabaritos](#baixar-provas-e-gabaritos)
23
+ - [Arquitetura](#arquitetura)
24
+ - [Entidades de Domínio](#entidades-de-domínio)
25
+ - [Filtros disponíveis](#filtros-disponíveis)
26
+ - [Customizando adaptadores](#customizando-adaptadores)
27
+ - [Dependências](#dependências)
28
+
29
+ ---
30
+
31
+ ## Instalação
32
+
33
+ > **Nota:** A gem ainda não foi publicada no RubyGems. Para uso local ou como dependência privada, adicione ao seu `Gemfile`:
34
+
35
+ ```ruby
36
+ gem 'concurso_hub', path: '/caminho/para/concurso_test'
37
+ # ou via git:
38
+ # gem 'concurso_hub', github: 'seu-usuario/concurso_hub'
39
+ ```
40
+
41
+ Para instalar as dependências do projeto:
42
+
43
+ ```bash
44
+ bundle install
45
+ ```
46
+
47
+ ---
48
+
49
+ ## Uso via CLI
50
+
51
+ O projeto inclui uma interface de linha de comando para uso rápido e testes.
52
+
53
+ ```bash
54
+ # Listar concursos abertos (padrão)
55
+ ruby main.rb
56
+
57
+ # Filtrar por estado
58
+ ruby main.rb --estado SP
59
+
60
+ # Filtrar por nível de escolaridade
61
+ ruby main.rb --nivel Superior
62
+
63
+ # Busca por texto (instituição ou cargo)
64
+ ruby main.rb --busca "analista"
65
+
66
+ # Limitar número de resultados
67
+ ruby main.rb --limite 10
68
+
69
+ # Filtrar por ano de inscrição
70
+ ruby main.rb --ano 2026
71
+
72
+ # Combinar filtros
73
+ ruby main.rb --estado RJ --nivel Medio --limite 20
74
+
75
+ # Listar concursos encerrados (requer --busca)
76
+ ruby main.rb --encerrados --busca "policia federal"
77
+
78
+ # Ver o edital completo de um concurso
79
+ ruby main.rb --ver https://pciconcursos.com.br/concurso/...
80
+
81
+ # Baixar os PDFs do edital de um concurso
82
+ ruby main.rb --baixar https://pciconcursos.com.br/concurso/...
83
+
84
+ # Baixar provas e gabaritos anteriores
85
+ ruby main.rb --baixar-provas https://pciconcursos.com.br/concurso/...
86
+
87
+ # Especificar pasta de destino para downloads
88
+ ruby main.rb --baixar https://... --dir ~/Downloads/concursos
89
+
90
+ # Ajuda
91
+ ruby main.rb --help
92
+ ```
93
+
94
+ ---
95
+
96
+ ## Uso como gem em uma API backend
97
+
98
+ A maneira mais simples de usar a gem é através do módulo `ConcursoHub`, que expõe uma API de alto nível. Você só precisa adicionar o `require` e chamar os métodos — sem precisar saber nada sobre a arquitetura interna.
99
+
100
+ ```ruby
101
+ require 'concurso_hub'
102
+ ```
103
+
104
+ ---
105
+
106
+ ### ConcursoHub.search
107
+
108
+ Busca concursos com filtros opcionais. Retorna um hash com `concursos` (array) e `metadata`.
109
+
110
+ ```ruby
111
+ # Busca simples — 1 requisição HTTP
112
+ resultado = ConcursoHub.search(estado: 'SP', nivel: 'Superior', limite: 10)
113
+
114
+ resultado[:concursos]
115
+ # => [
116
+ # { instituicao: "TRF-3", estado: "SP", vagas: "50", salario: "R$ 8.529",
117
+ # cargos: "Analista Judiciário", nivel: "Superior",
118
+ # prazo: "10/06/2026", url: "https://pciconcursos.com.br/concurso/..." },
119
+ # ...
120
+ # ]
121
+
122
+ resultado[:metadata]
123
+ # => { total_scraped: 120, modo: :abertos }
124
+ ```
125
+
126
+ **Com dados do edital** — inclui PDFs e descrição para cada concurso (`+1 req por concurso`):
127
+
128
+ ```ruby
129
+ resultado = ConcursoHub.search(estado: 'RJ', limite: 5, with_edital: true)
130
+
131
+ resultado[:concursos].first[:edital]
132
+ # => {
133
+ # titulo: "Edital nº 1/2026 — TRF-2",
134
+ # descricao: "...",
135
+ # data_publicacao: "15/04/2026",
136
+ # pdfs: [
137
+ # { titulo: "Edital completo", url: "https://..." },
138
+ # { titulo: "Retificação nº 1", url: "https://..." }
139
+ # ],
140
+ # provas_url: "https://pciconcursos.com.br/provas/...", # nil se não houver
141
+ # blocos: [...],
142
+ # url: "https://pciconcursos.com.br/concurso/..."
143
+ # }
144
+ ```
145
+
146
+ **Com provas e gabaritos** — inclui tudo acima mais a listagem de provas anteriores (`+N reqs por concurso` — use `limite`):
147
+
148
+ ```ruby
149
+ resultado = ConcursoHub.search(estado: 'SP', limite: 2, with_provas: true)
150
+
151
+ resultado[:concursos].first[:provas]
152
+ # => [
153
+ # {
154
+ # cargo: "Analista Judiciário — Área Administrativa",
155
+ # pdfs: [
156
+ # { titulo: "Prova Objetiva — 2023", url: "https://..." },
157
+ # { titulo: "Gabarito Preliminar — 2023", url: "https://..." }
158
+ # ]
159
+ # },
160
+ # ...
161
+ # ]
162
+ ```
163
+
164
+ **Parâmetros disponíveis:**
165
+
166
+ | Parâmetro | Tipo | Padrão | Descrição |
167
+ |---------------|-------------------|------------|---------------------------------------------------------|
168
+ | `estado` | `String \| nil` | `nil` | UF (ex: `'SP'`, `'MG'`) |
169
+ | `nivel` | `String \| nil` | `nil` | Escolaridade (ex: `'Superior'`, `'Médio'`) |
170
+ | `busca` | `String \| nil` | `nil` | Texto livre — obrigatório para `modo: :encerrados` |
171
+ | `limite` | `Integer \| nil` | `nil` | Máximo de resultados |
172
+ | `ano` | `String \| nil` | `nil` | Ano do prazo de inscrição (ex: `'2026'`) |
173
+ | `modo` | `Symbol` | `:abertos` | `:abertos` ou `:encerrados` |
174
+ | `with_edital` | `Boolean` | `false` | Inclui edital com PDFs (+1 req/concurso) |
175
+ | `with_provas` | `Boolean` | `false` | Inclui provas/gabaritos — implica `with_edital` |
176
+
177
+ ---
178
+
179
+ ### ConcursoHub.edital
180
+
181
+ Retorna o edital completo de um concurso a partir da URL.
182
+
183
+ ```ruby
184
+ edital = ConcursoHub.edital('https://pciconcursos.com.br/concurso/...')
185
+
186
+ edital[:titulo] # => "Edital nº 1/2026"
187
+ edital[:data_publicacao] # => "15/04/2026"
188
+ edital[:descricao] # => "..."
189
+ edital[:pdfs] # => [{ titulo:, url: }, ...]
190
+ edital[:blocos] # => [{ tipo: :secao|:paragrafo|:item, texto: }, ...]
191
+ edital[:url] # => URL original
192
+ ```
193
+
194
+ ---
195
+
196
+ ### ConcursoHub.provas
197
+
198
+ Retorna a listagem de provas e gabaritos anteriores. Recebe a mesma URL do concurso retornada pelo `search` — não precisa passar nenhuma URL intermediária.
199
+
200
+ ```ruby
201
+ # url vem diretamente de search()
202
+ concurso_url = resultado[:concursos].first[:url]
203
+
204
+ provas = ConcursoHub.provas(concurso_url)
205
+
206
+ provas
207
+ # => [
208
+ # {
209
+ # cargo: "Analista Judiciário — Área Administrativa",
210
+ # pdfs: [
211
+ # { titulo: "Prova Objetiva — 2023", url: "https://..." },
212
+ # { titulo: "Gabarito Preliminar — 2023", url: "https://..." }
213
+ # ]
214
+ # }
215
+ # ]
216
+ ```
217
+
218
+ ---
219
+
220
+ ### Exemplo completo em Rails
221
+
222
+ **`Gemfile`**
223
+ ```ruby
224
+ gem 'concurso_hub', path: '../concurso_test'
225
+ # ou após publicar: gem 'concurso_hub', '~> 1.0'
226
+ ```
227
+
228
+ **`app/controllers/concursos_controller.rb`**
229
+ ```ruby
230
+ require 'concurso_hub'
231
+
232
+ class ConcursosController < ApplicationController
233
+ # GET /concursos?estado=SP&nivel=Superior&limite=10
234
+ def index
235
+ resultado = ConcursoHub.search(
236
+ estado: params[:estado],
237
+ nivel: params[:nivel],
238
+ busca: params[:busca],
239
+ limite: params[:limite]&.to_i,
240
+ ano: params[:ano]
241
+ )
242
+ render json: resultado
243
+ rescue => e
244
+ render json: { error: e.message }, status: :bad_gateway
245
+ end
246
+
247
+ # GET /concursos/edital?url=https://pciconcursos.com.br/concurso/...
248
+ def edital
249
+ render json: ConcursoHub.edital(params[:url])
250
+ rescue => e
251
+ render json: { error: e.message }, status: :bad_gateway
252
+ end
253
+
254
+ # GET /concursos/provas?url=https://pciconcursos.com.br/concurso/...
255
+ # (mesma url retornada pelo /concursos)
256
+ def provas
257
+ render json: ConcursoHub.provas(params[:url])
258
+ rescue => e
259
+ render json: { error: e.message }, status: :bad_gateway
260
+ end
261
+ end
262
+ ```
263
+
264
+ **`config/routes.rb`**
265
+ ```ruby
266
+ get '/concursos', to: 'concursos#index'
267
+ get '/concursos/edital', to: 'concursos#edital'
268
+ get '/concursos/provas', to: 'concursos#provas' # ?url= é a URL do concurso
269
+ ```
270
+
271
+ ---
272
+
273
+ ## Uso avançado (injeção de dependências)
274
+
275
+ Se precisar de mais controle — mock para testes, presenter customizado, troca de repositório — você pode instanciar os use cases diretamente.
276
+
277
+ ### Listar concursos
278
+
279
+ ```ruby
280
+ require 'concurso_hub' # ou os requires individuais de src/
281
+
282
+ # Presenter que coleta os dados em vez de imprimir
283
+ class MeuPresenter < Application::Ports::Presenter
284
+ attr_reader :concursos, :metadata
285
+
286
+ def show(concursos, metadata: {}) = (@concursos = concursos) && (@metadata = metadata)
287
+ def error(msg) = raise msg
288
+ def show_loading = nil
289
+ def show_edital(_) = nil
290
+ def show_provas(_) = nil
291
+ def show_download_start(*) = nil
292
+ def show_download_done(_) = nil
293
+ end
294
+
295
+ repository = Infrastructure::Repositories::PciConcursoRepository.new
296
+ presenter = MeuPresenter.new
297
+
298
+ Application::UseCases::ListarConcursos.new(
299
+ repository: repository,
300
+ presenter: presenter
301
+ ).execute(
302
+ Application::FiltrosConcurso.new(estado: 'SP', nivel: 'Superior', limite: 10, modo: :abertos)
303
+ )
304
+
305
+ presenter.concursos # Array<Domain::Entities::Concurso>
306
+ presenter.metadata # Hash
307
+ ```
308
+
309
+ ### Ver edital completo
310
+
311
+ ```ruby
312
+ class EditalPresenter < Application::Ports::Presenter
313
+ attr_reader :edital
314
+
315
+ def show_edital(edital) = @edital = edital
316
+ def error(msg) = raise msg
317
+ def show(*) = nil
318
+ def show_loading = nil
319
+ def show_provas(_) = nil
320
+ def show_download_start(*) = nil
321
+ def show_download_done(_) = nil
322
+ end
323
+
324
+ presenter = EditalPresenter.new
325
+ Application::UseCases::VerEdital.new(
326
+ repository: Infrastructure::Repositories::PciConcursoRepository.new,
327
+ presenter: presenter
328
+ ).execute(Application::VerEditalRequest.new(url: 'https://pciconcursos.com.br/concurso/...'))
329
+
330
+ presenter.edital.titulo # => String
331
+ presenter.edital.pdfs # => [{ titulo:, url: }]
332
+ presenter.edital.provas_url # => String | nil
333
+ ```
334
+
335
+ ### Baixar PDFs de um edital
336
+
337
+ ```ruby
338
+ require 'application/use_cases/baixar_edital'
339
+ require 'application/baixar_edital_request'
340
+ require 'infrastructure/http/http_file_downloader'
341
+
342
+ Application::UseCases::BaixarEdital.new(
343
+ repository: Infrastructure::Repositories::PciConcursoRepository.new,
344
+ downloader: Infrastructure::Http::HttpFileDownloader.new,
345
+ presenter: presenter # qualquer presenter com show_download_start/done
346
+ ).execute(
347
+ Application::BaixarEditalRequest.new(
348
+ url: 'https://pciconcursos.com.br/concurso/...',
349
+ dest_dir: '/tmp/editais'
350
+ )
351
+ )
352
+ # PDFs salvos em dest_dir
353
+ ```
354
+
355
+ ### Baixar provas e gabaritos
356
+
357
+ ```ruby
358
+ require 'application/use_cases/baixar_provas'
359
+ require 'application/baixar_provas_request'
360
+
361
+ Application::UseCases::BaixarProvas.new(
362
+ repository: Infrastructure::Repositories::PciConcursoRepository.new,
363
+ downloader: Infrastructure::Http::HttpFileDownloader.new,
364
+ presenter: presenter
365
+ ).execute(
366
+ Application::BaixarProvasRequest.new(
367
+ url: 'https://pciconcursos.com.br/concurso/...', # mesma URL do concurso
368
+ dest_dir: '/tmp/provas'
369
+ )
370
+ )
371
+ # PDFs salvos em dest_dir organizados por cargo
372
+ ```
373
+
374
+ ---
375
+
376
+ ## Arquitetura
377
+
378
+ O projeto segue a **arquitetura hexagonal (Ports & Adapters)** com separação estrita de camadas:
379
+
380
+ ```
381
+ ┌─────────────────────────────────────────────────────┐
382
+ │ Apresentação (Adaptadores de entrada) │
383
+ │ CLI: CliOptionsParser → CliController │
384
+ │ API: Controller Rails/Sinatra/etc. (você implementa)│
385
+ │ TerminalPresenter ←→ JsonPresenter (custom) │
386
+ └──────────────────────┬──────────────────────────────┘
387
+ │ Request objects
388
+ ┌──────────────────────▼──────────────────────────────┐
389
+ │ Aplicação (Núcleo — sem dependências de framework) │
390
+ │ Use Cases: ListarConcursos, VerEdital, │
391
+ │ BaixarEdital, BaixarProvas │
392
+ │ Ports (interfaces): ConcursoRepository, │
393
+ │ FileDownloader, Presenter │
394
+ │ Value Objects: FiltrosConcurso, *Request │
395
+ └──────────────────────┬──────────────────────────────┘
396
+ │ Entidades de domínio
397
+ ┌──────────────────────▼──────────────────────────────┐
398
+ │ Domínio (Puro, sem dependências externas) │
399
+ │ Entities: Concurso, Edital (imutáveis/frozen) │
400
+ └──────────────────────┬──────────────────────────────┘
401
+ │ implementa ports
402
+ ┌──────────────────────▼──────────────────────────────┐
403
+ │ Infraestrutura (Adaptadores de saída) │
404
+ │ PciConcursoRepository (implementa ConcursoRepository)│
405
+ │ HttpClient, HttpFileDownloader │
406
+ │ PciHtmlParser (Nokogiri) │
407
+ └─────────────────────────────────────────────────────┘
408
+ ```
409
+
410
+ **Princípio central:** os use cases dependem apenas das interfaces (ports) — nunca de infraestrutura diretamente. Isso permite trocar a fonte de dados (outro site, banco de dados, mock) sem alterar o núcleo.
411
+
412
+ ---
413
+
414
+ ## Entidades de Domínio
415
+
416
+ ### `Domain::Entities::Concurso`
417
+
418
+ | Atributo | Tipo | Descrição |
419
+ |--------------|----------|--------------------------------------------|
420
+ | `instituicao`| `String` | Nome do órgão/instituição |
421
+ | `estado` | `String` | UF (ex: `SP`, `RJ`) |
422
+ | `vagas` | `String` | Número de vagas |
423
+ | `salario` | `String` | Faixa salarial |
424
+ | `cargos` | `String` | Cargos disponíveis |
425
+ | `nivel` | `String` | Nível de escolaridade exigido |
426
+ | `prazo` | `String` | Data-limite de inscrição |
427
+ | `url` | `String` | URL do edital no PCI Concursos |
428
+
429
+ ### `Domain::Entities::Edital`
430
+
431
+ | Atributo | Tipo | Descrição |
432
+ |-------------------|------------------|------------------------------------------------------|
433
+ | `titulo` | `String` | Título do edital |
434
+ | `descricao` | `String` | Descrição resumida |
435
+ | `data_publicacao` | `String` | Data de publicação |
436
+ | `blocos` | `Array<Hash>` | Conteúdo estruturado: `{ tipo:, texto: }` |
437
+ | `pdfs` | `Array<Hash>` | Lista de PDFs: `{ titulo:, url: }` |
438
+ | `provas_url` | `String \| nil` | URL da página de provas anteriores (se disponível) |
439
+ | `url` | `String` | URL canônica do edital |
440
+
441
+ Os tipos de bloco em `blocos` são: `:secao`, `:paragrafo`, `:item`.
442
+
443
+ ---
444
+
445
+ ## Filtros disponíveis
446
+
447
+ `Application::FiltrosConcurso` é um `Struct` com os seguintes campos:
448
+
449
+ | Campo | Tipo | Descrição | Exemplo |
450
+ |----------|-------------------|--------------------------------------------------|-----------------|
451
+ | `estado` | `String \| nil` | Filtrar por UF | `'SP'`, `'MG'` |
452
+ | `nivel` | `String \| nil` | Nível de escolaridade | `'Superior'` |
453
+ | `busca` | `String \| nil` | Busca textual (obrigatório para `:encerrados`) | `'analista'` |
454
+ | `limite` | `Integer \| nil` | Máximo de resultados retornados | `10` |
455
+ | `modo` | `Symbol` | `:abertos` (padrão) ou `:encerrados` | `:abertos` |
456
+ | `ano` | `String \| nil` | Ano do prazo de inscrição | `'2026'` |
457
+
458
+ ---
459
+
460
+ ## Customizando adaptadores
461
+
462
+ A arquitetura foi pensada para facilitar extensão. Você pode substituir qualquer adaptador sem tocar no núcleo.
463
+
464
+ ### Repositório mock para testes
465
+
466
+ ```ruby
467
+ require 'application/ports/concurso_repository'
468
+ require 'domain/entities/concurso'
469
+
470
+ class MockConcursoRepository < Application::Ports::ConcursoRepository
471
+ def fetch_abertos
472
+ concursos = [
473
+ Domain::Entities::Concurso.new(
474
+ instituicao: 'TRF-1',
475
+ estado: 'DF',
476
+ vagas: '50',
477
+ salario: 'R$ 8.000',
478
+ cargos: 'Analista Judiciário',
479
+ nivel: 'Superior',
480
+ prazo: '30/06/2026',
481
+ url: 'https://example.com/concurso'
482
+ )
483
+ ]
484
+ [concursos, { total_scraped: 1, modo: :abertos }]
485
+ end
486
+
487
+ # Implementar outros métodos conforme necessário para os testes...
488
+ end
489
+
490
+ # Injetar na ConcursoHub ou diretamente nos use cases
491
+ resultado = ConcursoHub.search(estado: 'DF')
492
+ # Para usar o mock, injete via uso direto dos use cases (seção anterior)
493
+ ```
494
+
495
+ ---
496
+
497
+ ## Dependências
498
+
499
+ | Gem | Versão | Uso |
500
+ |------------|----------|------------------------------|
501
+ | `nokogiri` | `~> 1.16`| Parsing de HTML |
502
+
503
+ Todo o restante utiliza apenas a biblioteca padrão do Ruby: `net/http`, `uri`, `optparse`.
504
+
505
+ **Versão mínima de Ruby:** 3.1 (usa pattern matching e hash shorthand syntax).
506
+
507
+ ---
508
+
509
+ ## Contribuindo
510
+
511
+ Pull requests são bem-vindos. Ao adicionar suporte a uma nova fonte de dados (além do PCI Concursos), implemente a interface `Application::Ports::ConcursoRepository` e injete o novo adaptador — o núcleo não precisa ser alterado.
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConcursoHub
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'concurso_hub/version'
4
+
5
+ $LOAD_PATH.unshift(File.join(__dir__, '..', 'src'))
6
+
7
+ require 'application/filtros_concurso'
8
+ require 'application/ver_edital_request'
9
+ require 'application/ports/presenter'
10
+ require 'application/use_cases/listar_concursos'
11
+ require 'application/use_cases/ver_edital'
12
+ require 'application/use_cases/listar_provas'
13
+ require 'infrastructure/repositories/pci_concurso_repository'
14
+
15
+ module ConcursoHub
16
+ def self.search(
17
+ estado: nil,
18
+ nivel: nil,
19
+ busca: nil,
20
+ limite: nil,
21
+ ano: nil,
22
+ modo: :abertos,
23
+ with_edital: false,
24
+ with_provas: false
25
+ )
26
+ repository = build_repository
27
+ presenter = SilentPresenter.new
28
+
29
+ Application::UseCases::ListarConcursos.new(
30
+ repository: repository,
31
+ presenter: presenter
32
+ ).execute(
33
+ Application::FiltrosConcurso.new(
34
+ estado: estado, nivel: nivel, busca: busca,
35
+ limite: limite, ano: ano, modo: modo
36
+ )
37
+ )
38
+
39
+ raise presenter.erro if presenter.erro
40
+
41
+ concursos = presenter.concursos.map { |c| serialize_concurso(c) }
42
+
43
+ if with_edital || with_provas
44
+ concursos.each do |c|
45
+ edital_data = fetch_edital_hash(c[:url], repository)
46
+ c[:edital] = edital_data
47
+
48
+ if with_provas && edital_data[:provas_url]
49
+ c[:provas] = fetch_provas_array(edital_data[:provas_url], repository)
50
+ else
51
+ c[:provas] = [] if with_provas
52
+ end
53
+ end
54
+ end
55
+
56
+ { concursos: concursos, metadata: presenter.metadata }
57
+ end
58
+
59
+ def self.edital(url)
60
+ fetch_edital_hash(url, build_repository)
61
+ end
62
+
63
+ def self.provas(url)
64
+ repository = build_repository
65
+ edital = fetch_edital_hash(url, repository)
66
+
67
+ raise "Nenhuma prova disponível para este concurso." unless edital[:provas_url]
68
+
69
+ fetch_provas_array(edital[:provas_url], repository)
70
+ end
71
+
72
+ private_class_method def self.build_repository
73
+ Infrastructure::Repositories::PciConcursoRepository.new
74
+ end
75
+
76
+ private_class_method def self.fetch_edital_hash(url, repository)
77
+ presenter = SilentPresenter.new
78
+ Application::UseCases::VerEdital.new(
79
+ repository: repository,
80
+ presenter: presenter
81
+ ).execute(Application::VerEditalRequest.new(url: url))
82
+
83
+ raise presenter.erro if presenter.erro
84
+
85
+ serialize_edital(presenter.edital)
86
+ end
87
+
88
+ private_class_method def self.fetch_provas_array(url, repository)
89
+ presenter = SilentPresenter.new
90
+ Application::UseCases::ListarProvas.new(
91
+ repository: repository,
92
+ presenter: presenter
93
+ ).execute(Application::VerEditalRequest.new(url: url))
94
+
95
+ return [] if presenter.erro
96
+
97
+ presenter.provas
98
+ end
99
+
100
+ private_class_method def self.serialize_concurso(c)
101
+ {
102
+ instituicao: c.instituicao,
103
+ estado: c.estado,
104
+ vagas: c.vagas,
105
+ salario: c.salario,
106
+ cargos: c.cargos,
107
+ nivel: c.nivel,
108
+ prazo: c.prazo,
109
+ url: c.url
110
+ }
111
+ end
112
+
113
+ private_class_method def self.serialize_edital(e)
114
+ {
115
+ titulo: e.titulo,
116
+ descricao: e.descricao,
117
+ data_publicacao: e.data_publicacao,
118
+ pdfs: e.pdfs,
119
+ provas_url: e.provas_url,
120
+ blocos: e.blocos,
121
+ url: e.url
122
+ }
123
+ end
124
+
125
+ class SilentPresenter < Application::Ports::Presenter
126
+ attr_reader :concursos, :edital, :provas, :metadata, :erro
127
+
128
+ def show(concursos, metadata: {})
129
+ @concursos = concursos
130
+ @metadata = metadata
131
+ end
132
+
133
+ def show_edital(edital) = @edital = edital
134
+ def show_provas(provas) = @provas = provas
135
+ def error(message) = @erro = message
136
+
137
+ def show_loading = nil
138
+ def show_download_start(*) = nil
139
+ def show_download_done(_) = nil
140
+ end
141
+ private_constant :SilentPresenter
142
+ end
143
+
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Application
4
+ BaixarEditalRequest = Struct.new(:url, :dest_dir, keyword_init: true)
5
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Application
4
+ BaixarProvasRequest = Struct.new(:url, :dest_dir, keyword_init: true)
5
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Application
4
+ FiltrosConcurso = Struct.new(
5
+ :estado,
6
+ :nivel,
7
+ :busca,
8
+ :limite,
9
+ :modo,
10
+ :ano,
11
+ keyword_init: true
12
+ ) do
13
+ def abertos? = modo != :encerrados
14
+ def encerrados? = modo == :encerrados
15
+ end
16
+ end