arbetsformedlingen 0.5.0 → 0.6.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 (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