arbetsformedlingen 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +6 -0
  3. data/.ruby-style-guide.yml +270 -0
  4. data/.travis.yml +0 -1
  5. data/CHANGELOG.md +12 -0
  6. data/Gemfile +2 -0
  7. data/LICENSE.txt +1 -1
  8. data/README.md +1 -1
  9. data/Rakefile +2 -0
  10. data/arbetsformedlingen.gemspec +10 -9
  11. data/bin/console +1 -0
  12. data/lib/arbetsformedlingen/api/client.rb +48 -30
  13. data/lib/arbetsformedlingen/api/ledigtarbete_client.rb +6 -1
  14. data/lib/arbetsformedlingen/api/matchning_client.rb +16 -10
  15. data/lib/arbetsformedlingen/api/request.rb +10 -13
  16. data/lib/arbetsformedlingen/api/response.rb +71 -0
  17. data/lib/arbetsformedlingen/api/results/ad_result.rb +36 -18
  18. data/lib/arbetsformedlingen/api/results/matchning_result.rb +42 -3
  19. data/lib/arbetsformedlingen/api/results/soklista_result.rb +16 -2
  20. data/lib/arbetsformedlingen/api/soap_request.rb +11 -10
  21. data/lib/arbetsformedlingen/api/values/ad_result_values.rb +6 -1
  22. data/lib/arbetsformedlingen/api/values/create_ad_page.rb +3 -1
  23. data/lib/arbetsformedlingen/api/values/matchning_result_values.rb +9 -1
  24. data/lib/arbetsformedlingen/api/values/soklista_values.rb +17 -1
  25. data/lib/arbetsformedlingen/api/ws_occupation_client.rb +26 -1
  26. data/lib/arbetsformedlingen/codes/country_code.rb +3 -1
  27. data/lib/arbetsformedlingen/codes/drivers_license_code.rb +6 -3
  28. data/lib/arbetsformedlingen/codes/experience_required_code.rb +3 -1
  29. data/lib/arbetsformedlingen/codes/municipality_code.rb +3 -1
  30. data/lib/arbetsformedlingen/codes/occupation_code.rb +3 -1
  31. data/lib/arbetsformedlingen/codes/salary_type_code.rb +3 -1
  32. data/lib/arbetsformedlingen/key_struct.rb +5 -3
  33. data/lib/arbetsformedlingen/models/application_method.rb +3 -1
  34. data/lib/arbetsformedlingen/models/company.rb +2 -2
  35. data/lib/arbetsformedlingen/models/document.rb +4 -2
  36. data/lib/arbetsformedlingen/models/dry/predicates.rb +2 -0
  37. data/lib/arbetsformedlingen/models/dry/types.rb +5 -3
  38. data/lib/arbetsformedlingen/models/model.rb +2 -0
  39. data/lib/arbetsformedlingen/models/packet.rb +3 -1
  40. data/lib/arbetsformedlingen/models/packet_xml_builder.rb +8 -3
  41. data/lib/arbetsformedlingen/models/position.rb +5 -3
  42. data/lib/arbetsformedlingen/models/publication.rb +3 -1
  43. data/lib/arbetsformedlingen/models/qualification.rb +3 -1
  44. data/lib/arbetsformedlingen/models/salary.rb +3 -1
  45. data/lib/arbetsformedlingen/models/schedule.rb +4 -2
  46. data/lib/arbetsformedlingen/soap_builder.rb +3 -1
  47. data/lib/arbetsformedlingen/version.rb +3 -1
  48. metadata +43 -25
@@ -1,13 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'arbetsformedlingen/api/request'
2
4
  require 'arbetsformedlingen/api/values/create_ad_page'
3
5
 
4
6
  module Arbetsformedlingen
5
7
  module API
8
+ # API client for ledigtarbete
6
9
  class LedigtarbeteClient
10
+ # Base URL for ledigtarbete
7
11
  BASE_URL = 'http://api.arbetsformedlingen.se/ledigtarbete'.freeze
8
12
 
13
+ # HTTP headers
9
14
  HEADERS = {
10
- 'Content-type' => 'text/xml'
15
+ 'Content-type' => 'text/xml',
11
16
  }.freeze
12
17
 
13
18
  # Post ad to API
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'date'
2
4
  require 'time'
3
5
  require 'arbetsformedlingen/api/request'
@@ -5,13 +7,16 @@ require 'arbetsformedlingen/api/results/matchning_result'
5
7
 
6
8
  module Arbetsformedlingen
7
9
  module API
10
+ # API client for matchning
8
11
  class MatchningClient
9
12
  attr_reader :request
10
13
 
14
+ # Initialize client
11
15
  def initialize(request: Request.new)
12
16
  @request = request
13
17
  end
14
18
 
19
+ # rubocop:disable Metrics/LineLength
15
20
  # Find matching ads from API
16
21
  # @return [MatchningResult] the result.
17
22
  # @param area_id [String] Area ID.
@@ -36,6 +41,7 @@ module Arbetsformedlingen
36
41
  # client.ads(keywrods: 'ruby', page: 3, page_size: 10)
37
42
  # @example Get ads with keyword and organsiation numer
38
43
  # client.ads(keywrods: 'ruby', organization_number: org_no)
44
+ # rubocop:enable Metrics/LineLength
39
45
  def ads(
40
46
  # one of these must be present
41
47
  county_id: nil,
@@ -55,7 +61,7 @@ module Arbetsformedlingen
55
61
 
56
62
  one_of_required = [county_id, municipality_id, occupation_id, keywords]
57
63
  if one_of_required.all?(&:nil?)
58
- error_message = 'One of: county_id, municipality_id, occupation_id, keywords is required'
64
+ error_message = 'One of: county_id, municipality_id, occupation_id, keywords is required' # rubocop:disable Metrics/LineLength
59
65
  raise ArgumentError, error_message
60
66
  end
61
67
 
@@ -77,12 +83,12 @@ module Arbetsformedlingen
77
83
  anstallningstyp: santize_employment_type_query(employment_type),
78
84
  yrkesomradeid: occupation_field_id,
79
85
  sokdatum: normalize_date_to_iso8601(published_after),
80
- organisationsnummer: organization_number
86
+ organisationsnummer: organization_number,
81
87
  }
82
88
 
83
89
  response = request.get('matchning', query: query)
84
90
 
85
- MatchningResult.build(response.json)
91
+ MatchningResult.build(response)
86
92
  end
87
93
 
88
94
  private
@@ -99,11 +105,11 @@ module Arbetsformedlingen
99
105
  end
100
106
 
101
107
  def santize_employment_type_query(employment_type)
102
- # Sökkriterier anställningstyp.
103
- # Värdena ska ligga mellan 1 och 3.
108
+ # Sökkriterier anställningstyp.
109
+ # Värdena ska ligga mellan 1 och 3.
104
110
  # 1 är XXX (EJ DOKUMENTERAT)
105
- # 2 är somarjobb / feriejobb
106
- # 3 är utlandsjobb
111
+ # 2 är somarjobb / feriejobb
112
+ # 3 är utlandsjobb
107
113
 
108
114
  # TODO: The question is what we do if an invalid parameter is passed
109
115
  # should we crash?
@@ -113,15 +119,15 @@ module Arbetsformedlingen
113
119
 
114
120
  def santize_keywords_query(keywords)
115
121
  #
116
- # Sökord kan separeras eller kombineras med något av följande exempel:
117
- # mellanslag ( )
122
+ # Sökord kan separeras eller kombineras med något av följande exempel:
123
+ # mellanslag (" ")
118
124
  #
119
125
  # [Example]
120
126
  # /matchning?nyckelord="bagare""test"
121
127
  # /matchning?nyckelord="bagare"OR"test" /matchning?nyckelord="automatisk"AND"test"
122
128
 
123
129
  # Valid characters
124
- # abcdefghijklmnopqrstuvwxyzåäö0123456789: ,.-"
130
+ # abcdefghijklmnopqrstuvwxyzåäö0123456789: ,.-"
125
131
 
126
132
  # TODO: What do we do if invalid characters are passed? Crash?
127
133
  keywords
@@ -1,19 +1,26 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'uri'
2
4
  require 'net/http'
3
5
  require 'json'
6
+ require 'arbetsformedlingen/api/response'
4
7
 
5
8
  module Arbetsformedlingen
6
9
  module API
10
+ # API request object
7
11
  class Request
8
- Response = KeyStruct.new(:code, :body, :json)
9
-
10
12
  attr_reader :locale, :base_url
11
13
 
14
+ # Initialize request
12
15
  def initialize(base_url: '', locale: 'sv')
13
16
  @base_url = base_url
14
17
  @locale = locale
15
18
  end
16
19
 
20
+ # Perform GEt request
21
+ # @param [String] url to be fetched
22
+ # @param [Hash] query params
23
+ # @return [Response] response object
17
24
  def get(url, query: {})
18
25
  uri = URI("#{base_url}#{url}?#{URI.encode_www_form(query.to_a)}")
19
26
 
@@ -25,17 +32,7 @@ module Arbetsformedlingen
25
32
 
26
33
  response = http.request(request)
27
34
 
28
- Response.new(
29
- code: response.code,
30
- body: response.read_body,
31
- json: parse_json(response.read_body)
32
- )
33
- end
34
-
35
- def parse_json(string)
36
- JSON.parse(string.to_s)
37
- rescue JSON::ParserError => _e
38
- {}
35
+ Response.new(response)
39
36
  end
40
37
  end
41
38
  end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'nokogiri'
5
+ rescue LoadError
6
+ end
7
+
8
+ module Arbetsformedlingen
9
+ module API
10
+ # API response object
11
+ class Response
12
+ # Initialize response
13
+ def initialize(response)
14
+ @response = response
15
+ @json = nil
16
+ end
17
+
18
+ # True if response is 200
19
+ # @return [Boolean] true if response code is 200
20
+ def success?
21
+ response.code == '200'
22
+ end
23
+
24
+ # Response body
25
+ # @return [String] the response body
26
+ def body
27
+ response.read_body
28
+ end
29
+
30
+ # Response JSON
31
+ # @return [Hash] response json - empty if JSON is invalid
32
+ def json
33
+ @json ||= parse_json(body)
34
+ end
35
+
36
+ # Response XML
37
+ # @return [Nokogiri::XML::Document] response - empty is XML is invalid
38
+ def xml
39
+ @xml ||= parse_xml(body)
40
+ end
41
+
42
+ # Delegate missing values to response
43
+ def method_missing(method_name, *arguments, &block)
44
+ if response.respond_to?(method_name)
45
+ response.public_send(method_name, *arguments, &block)
46
+ else
47
+ super
48
+ end
49
+ end
50
+
51
+ # Return true if missing method can be delegated
52
+ def respond_to_missing?(method_name, include_private = false)
53
+ response.respond_to?(method_name) || super
54
+ end
55
+
56
+ private
57
+
58
+ attr_reader :response
59
+
60
+ def parse_json(string)
61
+ JSON.parse(string.to_s)
62
+ rescue JSON::ParserError => _e
63
+ {}
64
+ end
65
+
66
+ def parse_xml(string)
67
+ Nokogiri::XML(string).tap(&:remove_namespaces!)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -1,11 +1,24 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'arbetsformedlingen/api/values/ad_result_values'
2
4
 
3
5
  module Arbetsformedlingen
4
6
  module API
5
7
  module AdResult
6
- def self.build(response_data)
7
- data = response_data.fetch('platsannons')
8
+ # Build API result object for ad result
9
+ # @param [API::Response] response
10
+ # @return [Values::Ad]
11
+ def self.build(response)
12
+ return build_empty(response) unless response.success?
13
+
14
+ build_page(response)
15
+ end
8
16
 
17
+ # private
18
+
19
+ def self.build_page(response)
20
+ response_data = response.json
21
+ data = response_data.fetch('platsannons')
9
22
  ad_data = data.fetch('annons')
10
23
 
11
24
  Values::Ad.new(
@@ -19,32 +32,37 @@ module Arbetsformedlingen
19
32
  total_vacancies: ad_data.fetch('antal_platser'),
20
33
  municipalities: ad_data.fetch('kommunnamn'),
21
34
  municipality_id: ad_data.fetch('kommunkod'),
22
- total_vacancies_with_visa: ad_data.fetch('antalplatserVisa'),
35
+ total_vacancies_with_visa: ad_data.fetch('antalplatserVisa', nil),
23
36
  employment_type: ad_data.fetch('anstallningstyp'),
24
37
  terms: build_terms(data.fetch('villkor')),
25
38
  application: build_application(data.fetch('ansokan')),
26
39
  workplace: build_workplace(data.fetch('arbetsplats')),
27
- requirements: build_requirements(data.fetch('krav'))
40
+ requirements: build_requirements(data.fetch('krav')),
41
+ response: response
28
42
  )
29
43
  end
30
44
 
45
+ def self.build_empty(response)
46
+ Values::Ad.new(response: response)
47
+ end
48
+
31
49
  def self.build_terms(data)
32
50
  Values::Terms.new(
33
- duration: data.fetch('varaktighet'),
34
- working_hours: data.fetch('arbetstid'),
35
- working_hours_description: data.fetch('arbetstidvaraktighet'),
51
+ duration: data.fetch('varaktighet', nil),
52
+ working_hours: data.fetch('arbetstid', nil),
53
+ working_hours_description: data.fetch('arbetstidvaraktighet', nil),
36
54
  salary_type: data.fetch('lonetyp'),
37
- salary_form: data.fetch('loneform')
55
+ salary_form: data.fetch('loneform', nil)
38
56
  )
39
57
  end
40
58
 
41
59
  def self.build_application(data)
42
60
  Values::Application.new(
43
61
  reference: data['referens'],
44
- application_url: data.fetch('webbplats'),
62
+ application_url: data.fetch('webbplats', nil),
45
63
  email: data['epostadress'],
46
64
  last_application_at: data.fetch('sista_ansokningsdag', nil),
47
- application_comment: data.fetch('ovrigt_om_ansokan')
65
+ application_comment: data.fetch('ovrigt_om_ansokan', nil)
48
66
  )
49
67
  end
50
68
 
@@ -53,11 +71,11 @@ module Arbetsformedlingen
53
71
  name: data.fetch('arbetsplatsnamn'),
54
72
  postal: build_postal(data),
55
73
  country: data.fetch('land'),
56
- visit_address: data.fetch('besoksadress'),
57
- logotype_url: data.fetch('logotypurl'),
58
- website: data.fetch('hemsida'),
74
+ visit_address: data.fetch('besoksadress', nil),
75
+ logotype_url: data.fetch('logotypurl', nil),
76
+ website: data.fetch('hemsida', nil),
59
77
  contacts: (
60
- data.dig('kontaktpersonlista', 'kontaktpersonlista') || []
78
+ data.dig('kontaktpersonlista', 'kontaktpersondata') || []
61
79
  ).map do |contact_data|
62
80
  build_workplace_contacts(contact_data)
63
81
  end
@@ -66,10 +84,10 @@ module Arbetsformedlingen
66
84
 
67
85
  def self.build_postal(data)
68
86
  Values::Postal.new(
69
- code: data.fetch('postnummer'),
70
- address: data.fetch('postadress'),
71
- city: data.fetch('postort'),
72
- country: data.fetch('postland')
87
+ code: data.fetch('postnummer', nil),
88
+ address: data.fetch('postadress', nil),
89
+ city: data.fetch('postort', nil),
90
+ country: data.fetch('postland', nil)
73
91
  )
74
92
  end
75
93
 
@@ -1,9 +1,39 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'arbetsformedlingen/api/values/matchning_result_values'
2
4
 
3
5
  module Arbetsformedlingen
4
6
  module API
5
7
  module MatchningResult
6
- def self.build(response_data)
8
+ # Build API result object for matchning result
9
+ # @param [API::Response] response
10
+ # @return [Values::MatchningPage]
11
+ def self.build(response)
12
+ return empty_matchning_page(response) unless response.success?
13
+
14
+ build_matchning_page(response)
15
+ end
16
+
17
+ # private
18
+
19
+ def self.empty_matchning_page(response)
20
+ response_data = response.json
21
+
22
+ Values::MatchningPage.new(
23
+ list_name: 'annonser',
24
+ total_ads: 0,
25
+ total_ads_exact: 0,
26
+ total_ads_nearby: 0,
27
+ total_vacancies_on_page: 0,
28
+ total_pages: 0,
29
+ raw_data: response_data,
30
+ data: [],
31
+ response: response
32
+ )
33
+ end
34
+
35
+ def self.build_matchning_page(response)
36
+ response_data = response.json
7
37
  data = response_data.fetch('matchningslista')
8
38
 
9
39
  Values::MatchningPage.new(
@@ -14,10 +44,19 @@ module Arbetsformedlingen
14
44
  total_vacancies_on_page: data.fetch('antal_platserTotal'),
15
45
  total_pages: data.fetch('antal_sidor'),
16
46
  raw_data: response_data,
17
- data: data.fetch('matchningdata').map { |ad_data| build_ad_result(ad_data) }
47
+ data: build_ad_results(data),
48
+ response: response
18
49
  )
19
50
  end
20
51
 
52
+ # private
53
+
54
+ def self.build_ad_results(data)
55
+ data.fetch('matchningdata', []).map do |ad_data|
56
+ build_ad_result(ad_data)
57
+ end
58
+ end
59
+
21
60
  def self.build_ad_result(ad_data)
22
61
  Values::MatchningAd.new(
23
62
  id: ad_data.fetch('annonsid'),
@@ -32,7 +71,7 @@ module Arbetsformedlingen
32
71
  url: ad_data.fetch('annonsurl'),
33
72
  relevance: ad_data.fetch('relevans'),
34
73
  total_vacancies: ad_data.fetch('antalplatser'),
35
- total_vacancies_with_visa: ad_data.fetch('antalPlatserVisa'),
74
+ total_vacancies_with_visa: ad_data.fetch('antalPlatserVisa', nil),
36
75
  duration_id: ad_data.fetch('varaktighetId', nil),
37
76
  counties: ad_data.fetch('lan'),
38
77
  country_id: ad_data.fetch('lanid'),
@@ -1,9 +1,22 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'arbetsformedlingen/api/values/soklista_values'
2
4
 
3
5
  module Arbetsformedlingen
4
6
  module API
5
7
  module SoklistaResult
6
- def self.build(response_data, list_name: nil)
8
+ # Build API result object for "soklista"
9
+ # @param [API::Response] response
10
+ # @param list_name [String] result list name
11
+ # @return [Values::SoklistaPage]
12
+ def self.build(response, list_name: nil)
13
+ build_page(response, list_name)
14
+ end
15
+
16
+ # private
17
+
18
+ def self.build_page(response, list_name)
19
+ response_data = response.json
7
20
  data = response_data.fetch('soklista', {})
8
21
 
9
22
  Values::SoklistaPage.new(
@@ -13,7 +26,8 @@ module Arbetsformedlingen
13
26
  raw_data: response_data,
14
27
  data: data.fetch('sokdata', []).map do |result|
15
28
  build_search_result(result)
16
- end
29
+ end,
30
+ response: response
17
31
  )
18
32
  end
19
33
 
@@ -1,5 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'uri'
2
4
  require 'net/http'
5
+ require 'arbetsformedlingen/api/response'
3
6
 
4
7
  begin
5
8
  require 'nokogiri'
@@ -8,11 +11,14 @@ end
8
11
 
9
12
  module Arbetsformedlingen
10
13
  module API
14
+ # API SOAP request
11
15
  class SOAPRequest
12
- Response = KeyStruct.new(:code, :body, :xml)
16
+ # SOAP response
17
+ # Response = KeyStruct.new(:code, :body, :xml)
13
18
 
14
19
  attr_reader :locale, :uri, :url
15
20
 
21
+ # Initialize SOAP request
16
22
  def initialize(url, locale: nil)
17
23
  unless Object.const_defined?(:Nokogiri)
18
24
  raise(ArgumentError, "unable to require 'nokogiri' gem, please install it")
@@ -23,6 +29,9 @@ module Arbetsformedlingen
23
29
  @locale = locale
24
30
  end
25
31
 
32
+ # Performs a POST request
33
+ # @param [String] the post body
34
+ # @return [Response] the response
26
35
  def post(body)
27
36
  http = Net::HTTP.new(uri.host, uri.port)
28
37
  http.use_ssl = true if uri.scheme == 'https'
@@ -34,15 +43,7 @@ module Arbetsformedlingen
34
43
 
35
44
  response = http.request(request)
36
45
 
37
- Response.new(
38
- code: response.code,
39
- body: response.read_body,
40
- xml: parse_xml(response.read_body)
41
- )
42
- end
43
-
44
- def parse_xml(string)
45
- Nokogiri::XML(string).tap { |doc| doc.remove_namespaces! }
46
+ Response.new(response)
46
47
  end
47
48
  end
48
49
  end