arbetsformedlingen 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2a4e58a260599331e94848908320e079550f9418
4
- data.tar.gz: ee0be5981d236c6e23bb0bf593b02f5c8874dffe
3
+ metadata.gz: c7974cf075a4b0845cf00e7c16a66f8d3112bdd3
4
+ data.tar.gz: a45d7d95bc4209c97f730f5f0711eff9f648fc7e
5
5
  SHA512:
6
- metadata.gz: b37da20b192b30d9b097673ffd104ed0851398adefcb93fce1c1790a0551ccde0f716876ee15f82d70e3ec267e09ef388fb717335c275171b2c794d2dbef4e50
7
- data.tar.gz: 7c5a7f8b2453d0bc84d8c813ae99a038cc59d1ada3d3dd28c64b81ff636253c427c4ded3794d4e6b433eb82bf2340d1fb6d861d0d97a0358f0a64f9831cfd6e8
6
+ metadata.gz: dcf06f2e719fc148022bf3bf18e66d029228e9c26ec657c6e6255a3f84b3a9a50bd1f4fac8f694dd9b3a047dab183d7d7a0c91b9424a168d444397b30e02ed8c
7
+ data.tar.gz: 34e13510b301ab04d176db5381144af159814c200fbc1e4d03725c0de1a17b0c55ab3065044b83e1edf7a63d4327e55cfe54e929601f56b4173fac3550e3f36b
data/.gitignore CHANGED
@@ -15,3 +15,5 @@
15
15
  example.rb
16
16
  output.xml
17
17
  test.xml
18
+
19
+ spec/cassettes
data/README.md CHANGED
@@ -1,6 +1,16 @@
1
- # Arbetsförmedlingen
1
+ # Arbetsförmedlingen [![Build Status](https://travis-ci.org/buren/arbetsformedlingen.svg?branch=master)](https://travis-ci.org/buren/arbetsformedlingen)
2
2
 
3
- Post job ads to the Swedish employment agency (Arbetsförmedlingen).
3
+ Arbetsförmedlingen API client (Swedish Public Employment Service).
4
+
5
+ __Features__
6
+ * Post job ad (a.k.a Direktöverförda annonser)
7
+ * Platsannons API Client
8
+
9
+
10
+ __Index__
11
+ * [Installation](#installation)
12
+ * [API usage](#api-usage)
13
+ * [Post ad usage](#post-ad-usage)
4
14
 
5
15
  ## Installation
6
16
 
@@ -18,15 +28,47 @@ Or install it yourself as:
18
28
 
19
29
  $ gem install arbetsformedlingen
20
30
 
21
- ## Usage
31
+ ## API usage
32
+
33
+ __Create a client:__
34
+
35
+ ```ruby
36
+ client = Arbetsformedlingen::API::Client.new(locale: 'en')
37
+ ```
38
+
39
+ __Fetch all ads containing specified keyword:__
40
+ ```ruby
41
+ ads = client.ads(keywords: 'ruby')
42
+ ads.map(&:title)
43
+ ```
44
+
45
+ __Fetch one ad:__
46
+ ```ruby
47
+ ad = client.ad(id: 7408089)
48
+ ad.title
49
+ ad.occupation
50
+ ad.application.last_application_at
51
+ ```
52
+
53
+ __Fetch countries in area:__
54
+ ```ruby
55
+ countries = client.countries(area_id: 2)
56
+ countries.map do |country|
57
+ "#{country.name} has #{country.total_vacancies} total vacancies."
58
+ end
59
+ ```
60
+
61
+ ## Post ad usage
22
62
 
23
- __Complete example__
63
+ __Complete example creating a packet__
64
+
65
+ :information_source: There is quite a lot of data you can/must send to the API when creating an ad.
24
66
 
25
67
  ```ruby
26
68
  require 'date'
27
69
  require 'arbetsformedlingen'
28
70
 
29
- include Arbetsformedlingen
71
+ include Arbetsformedlingen # just for brevity
30
72
 
31
73
  document = Document.new(
32
74
  customer_id: 'XXXYYYZZZ',
@@ -125,8 +167,8 @@ puts "application_method.valid?: #{application_method.valid?}"
125
167
  puts "position.valid?: #{position.valid?}"
126
168
  puts "packet.valid?: #{packet.valid?}"
127
169
 
128
- output = OutputBuilder.new(packet)
129
- File.write('output.xml', output.to_xml)
170
+ client = API::Client.new(locale: 'sv')
171
+ client.create_ad(packet)
130
172
  ```
131
173
 
132
174
  ## Arbetsförmedlingen TaxonomyService
@@ -135,6 +177,22 @@ Some requests had to be made to Arbetsförmedlingens TaxonomyService in order to
135
177
 
136
178
  [![Postman](https://run.pstmn.io/button.svg)](https://app.getpostman.com/run-collection/9a27ec2518c1005f8aea)
137
179
 
180
+ ## Terms translation table
181
+
182
+ This gem has translated the attribute names in Arbetsförmedlingens (AF) API from Swedish to English. You can find the translations below.
183
+
184
+ | AF Term | Gem term |
185
+ |--------------------- |--------------------|
186
+ | landområde/värdsdel | areas |
187
+ | kommun | municipality |
188
+ | län | counties |
189
+ | län2 | counties2 |
190
+ | yrkesområde | occupational_fields |
191
+ | yrkesgrupp | occupational_group |
192
+ | yrkesgrupp | occupational_group |
193
+ | yrkesnamn | occupation |
194
+
195
+
138
196
  ## Development
139
197
 
140
198
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -29,5 +29,7 @@ Gem::Specification.new do |spec|
29
29
  spec.add_development_dependency 'bundler', '~> 1.14'
30
30
  spec.add_development_dependency 'rake', '~> 10.0'
31
31
  spec.add_development_dependency 'rspec', '~> 3.0'
32
+ spec.add_development_dependency 'webmock', '~> 3.1'
33
+ spec.add_development_dependency 'vcr', '~> 3.0'
32
34
  spec.add_development_dependency 'byebug'
33
35
  end
@@ -7,6 +7,8 @@ require 'dry-types'
7
7
 
8
8
  require 'arbetsformedlingen/version'
9
9
 
10
+ require 'arbetsformedlingen/key_struct'
11
+
10
12
  require 'arbetsformedlingen/codes/country_code'
11
13
  require 'arbetsformedlingen/codes/drivers_license_code'
12
14
  require 'arbetsformedlingen/codes/experience_required_code'
@@ -17,9 +19,6 @@ require 'arbetsformedlingen/codes/salary_type_code'
17
19
  require 'arbetsformedlingen/models/dry/types'
18
20
  require 'arbetsformedlingen/models/dry/predicates'
19
21
 
20
- require 'arbetsformedlingen/output_builder'
21
- require 'arbetsformedlingen/client'
22
-
23
22
  require 'arbetsformedlingen/models/model'
24
23
  require 'arbetsformedlingen/models/document'
25
24
  require 'arbetsformedlingen/models/company'
@@ -31,11 +30,10 @@ require 'arbetsformedlingen/models/schedule'
31
30
  require 'arbetsformedlingen/models/application_method'
32
31
  require 'arbetsformedlingen/models/packet'
33
32
 
34
- module Arbetsformedlingen
35
- def self.post_job(packet)
36
- Client.post_job(OutputBuilder.new(packet).to_xml)
37
- end
33
+ # API Client
34
+ require 'arbetsformedlingen/api/client'
38
35
 
36
+ module Arbetsformedlingen
39
37
  class << self
40
38
  attr_accessor :config
41
39
  end
@@ -0,0 +1,175 @@
1
+ require 'uri'
2
+ require 'net/http'
3
+ require 'json'
4
+
5
+ require 'arbetsformedlingen/api/request'
6
+
7
+ require 'arbetsformedlingen/api/results/ad_result'
8
+ require 'arbetsformedlingen/api/results/matchning_result'
9
+ require 'arbetsformedlingen/api/results/soklista_result'
10
+
11
+ # Sub-clients
12
+ require 'arbetsformedlingen/api/matchning_client'
13
+ require 'arbetsformedlingen/api/ledigtarbete_client'
14
+
15
+ module Arbetsformedlingen
16
+ module API
17
+ class Client
18
+ BASE_URL = 'http://api.arbetsformedlingen.se/af/v0/platsannonser/'.freeze
19
+
20
+ attr_reader :request, :locale
21
+
22
+ def initialize(locale: 'sv')
23
+ @request = Request.new(base_url: BASE_URL, locale: locale)
24
+ @locale = locale
25
+ end
26
+
27
+ # Get version of API
28
+ # @return [String] the version of the API.
29
+ # @example Get API version
30
+ # client.version
31
+ def version
32
+ request.get('version').body
33
+ end
34
+
35
+ # Post ad to API (ad => annons)
36
+ # @return [AdResult] the result.
37
+ # @param [Arbetsformedlingen::Packet] Packet object.
38
+ # @example Post ad
39
+ # client.ad(packet)
40
+ def create_ad(packet)
41
+ client = LedigtarbeteClient.new
42
+ client.create_ad(packet)
43
+ end
44
+
45
+ # Fetch ad from API (ad => annons)
46
+ # @return [AdResult] the result.
47
+ # @param id [String] Ad ID.
48
+ # @example Get ad
49
+ # client.ad(id: id)
50
+ def ad(id:)
51
+ response = request.get(id)
52
+
53
+ AdResult.build(response.json)
54
+ end
55
+
56
+ # Fetch areas from API (areas => landområde/värdsdel)
57
+ # @return [MatchningResult] the result.
58
+ # @see MatchningClient#ads
59
+ # @see MatchningResult#build
60
+ def ads(**args)
61
+ client = MatchningClient.new(request: request)
62
+ client.ads(**args)
63
+ end
64
+
65
+ # Fetch areas from API (areas => landområde/värdsdel)
66
+ # @return [AdResult] the result.
67
+ # @example Get areas
68
+ # client.areas
69
+ def areas
70
+ response = request.get('soklista/omrade')
71
+
72
+ SoklistaResult.build(response.json)
73
+ end
74
+
75
+ # Fetch counties from API (countries => land)
76
+ # @return [AdResult] the result.
77
+ # @param area_id [String] Area ID.
78
+ # @example Get countries within area
79
+ # client.countries(area_id: id)
80
+ def countries(area_id:)
81
+ query = { omradeid: area_id }
82
+ response = request.get('soklista/land', query: query)
83
+
84
+ SoklistaResult.build(response.json)
85
+ end
86
+
87
+ # Fetch municipalities from API (municipality => kommun)
88
+ # @return [AdResult] the result.
89
+ # @param county_id [String] County ID.
90
+ # @example Get counties
91
+ # client.counties
92
+ def municipalities(county_id: nil)
93
+ # NOTE: Due to a quirck in the API the lanid-param
94
+ # *must* be present though it *can* be nil
95
+ query = { lanid: county_id }
96
+ response = request.get('soklista/kommuner', query: query)
97
+
98
+ SoklistaResult.build(response.json)
99
+ end
100
+
101
+ # Fetch counties from API (county => län)
102
+ # @return [AdResult] the result.
103
+ # @example Get counties
104
+ # client.counties
105
+ def counties
106
+ response = request.get('soklista/lan')
107
+
108
+ SoklistaResult.build(response.json)
109
+ end
110
+
111
+ # Fetch counties2 from API (county2 => län2)
112
+ # @return [AdResult] the result.
113
+ # @example Get counties2
114
+ # client.counties2
115
+ def counties2
116
+ response = request.get('soklista/lan2')
117
+
118
+ SoklistaResult.build(response.json)
119
+ end
120
+
121
+ # Fetch occupational fields from API (occupational_fields => yrkesområde)
122
+ # @return [AdResult] the result.
123
+ # @example Get occupational fields
124
+ # client.occupational_field
125
+ def occupational_fields
126
+ response = request.get('soklista/yrkesomraden')
127
+
128
+ SoklistaResult.build(response.json)
129
+ end
130
+
131
+ # Fetch occupational group from API (occupational_group => yrkesgrupp)
132
+ # @return [AdResult] the result.
133
+ # @param occupational_field_id [String] Occupational field ID.
134
+ # @example Get all occupational group
135
+ # client.occupational_group
136
+ # @example Get occupational group within occupational field
137
+ # client.occupational_group(occupational_field_id: id)
138
+ def occupational_group(occupational_field_id: nil)
139
+ # NOTE: Due to a quirck in the API the yrkesomradeid-param
140
+ # *must* be present though it *can* be nil
141
+ query = { yrkesomradeid: occupational_field_id }
142
+ response = request.get('soklista/yrkesgrupper', query: query)
143
+
144
+ SoklistaResult.build(response.json)
145
+ end
146
+
147
+ # Fetch occupation from API (occupation => yrkesnamn)
148
+ # @return [AdResult] the result.
149
+ # @param name [String] Name of the occupation.
150
+ # @example Get occupation
151
+ # client.occupation(name: 'Marknadskommunikatör')
152
+ def occupation(name:)
153
+ response = request.get("soklista/yrken/#{URI.encode(name)}")
154
+
155
+ SoklistaResult.build(response.json)
156
+ end
157
+
158
+ # Fetch occupations from API (occupation => yrkesnamn)
159
+ # @return [AdResult] the result.
160
+ # @param occupational_group_id [String] Occupational group ID.
161
+ # @example Get stats of available positions for all occupations
162
+ # client.occupations
163
+ # @example Get stats of available positions for some occupations
164
+ # client.occupations(occupational_group_id: id)
165
+ def occupations(occupational_group_id: nil)
166
+ # NOTE: Due to a quirck in the API the yrkesgruppid-param
167
+ # *must* be present though it *can* be nil
168
+ query = { yrkesgruppid: occupational_group_id }
169
+ response = request.get('soklista/yrken', query: query)
170
+
171
+ SoklistaResult.build(response.json)
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,30 @@
1
+ require 'arbetsformedlingen/api/request'
2
+ require 'arbetsformedlingen/api/values/create_ad_page'
3
+
4
+ module Arbetsformedlingen
5
+ module API
6
+ class LedigtarbeteClient
7
+ BASE_URL = 'http://api.arbetsformedlingen.se/ledigtarbete'.freeze
8
+
9
+ HEADERS = {
10
+ 'Content-type' => 'text/xml'
11
+ }.freeze
12
+
13
+ # Post ad to API
14
+ # @param [Arbetsformedlingen::Packet, #to_xml] the data to be sent
15
+ # @return [Values::CreateAdPage] the API result
16
+ def create_ad(packet)
17
+ xml = packet.to_xml
18
+
19
+ url = if Arbetsformedlingen.config.test
20
+ 'apiledigtarbete/test/hrxml'
21
+ else
22
+ 'apiledigtarbete/hrxml'
23
+ end
24
+
25
+ response = HTTParty.post("#{BASE_URL}/#{url}", body: xml, headers: HEADERS)
26
+ Values::CreateAdPage.new(response, xml)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,127 @@
1
+ require 'date'
2
+ require 'time'
3
+ require 'arbetsformedlingen/api/request'
4
+ require 'arbetsformedlingen/api/results/matchning_result'
5
+
6
+ module Arbetsformedlingen
7
+ module API
8
+ class MatchningClient
9
+ attr_reader :request
10
+
11
+ def initialize(request: Request.new)
12
+ @request = request
13
+ end
14
+
15
+ # Find matching ads from API
16
+ # @return [MatchningResult] the result.
17
+ # @param area_id [String] Area ID.
18
+ # @param county_id [String] County ID.
19
+ # @param municipality_id [String] Municipality ID.
20
+ # @param occupation_id [String] Occupation ID.
21
+ # @param keywords [String] Keywords.
22
+ # @param page [Integer] Page ID.
23
+ # @param page_size [Integer] Page size ID.
24
+ # @param occupation_group_id [String] Occupation_group ID.
25
+ # @param employment_type [String] Employment type ID.
26
+ # @param occupation_field_id [String] Occupation field ID.
27
+ # @param published_after [Time, Date, String] Published after ID (ISO8601 format: YYYY-MM-DDThh:mm:ssTZD).
28
+ # @param organization_number [String] Organization_number ID.
29
+ # @example Get ads within county
30
+ # client.ads(county: id)
31
+ # @example Get ads within municipality
32
+ # client.ads(municipality: id)
33
+ # @example Get ads with keyword
34
+ # client.ads(keywrods: 'ruby')
35
+ # @example Get ads with keyword on page 3 and with a page size of 10
36
+ # client.ads(keywrods: 'ruby', page: 3, page_size: 10)
37
+ # @example Get ads with keyword and organsiation numer
38
+ # client.ads(keywrods: 'ruby', organization_number: org_no)
39
+ def ads(
40
+ # one of these must be present
41
+ county_id: nil,
42
+ municipality_id: nil,
43
+ occupation_id: nil,
44
+ keywords: nil,
45
+ # optional
46
+ page: 1,
47
+ page_size: 30,
48
+ area_id: nil,
49
+ occupation_group_id: nil,
50
+ employment_type: nil,
51
+ occupation_field_id: nil,
52
+ published_after: nil,
53
+ organization_number: nil
54
+ )
55
+
56
+ one_of_required = [county_id, municipality_id, occupation_id, keywords]
57
+ if one_of_required.all?(&:nil?)
58
+ error_message = 'One of: county_id, municipality_id, occupation_id, keywords is required'
59
+ raise ArgumentError, error_message
60
+ end
61
+
62
+ # TODO: Should we validate the IDs passed? What if they're invalid? Do we crash?
63
+
64
+ query = {
65
+ lanid: county_id,
66
+ kommunid: municipality_id,
67
+ yrkesid: occupation_id,
68
+ nyckelord: santize_keywords_query(keywords),
69
+ sida: page,
70
+ antalrader: page_size,
71
+ omradeid: area_id,
72
+ yrkesgruppid: occupation_group_id,
73
+ anstallningstyp: santize_employment_type_query(employment_type),
74
+ yrkesomradeid: occupation_field_id,
75
+ sokdatum: normalize_date_to_iso8601(published_after),
76
+ organisationsnummer: organization_number
77
+ }
78
+
79
+ response = request.get('matchning', query: query)
80
+
81
+ MatchningResult.build(response.json)
82
+ end
83
+
84
+ private
85
+
86
+ # @raise [ArgumentError] raises error if passed invalid value
87
+ def normalize_date_to_iso8601(date_time_or_string)
88
+ return unless date_time_or_string
89
+
90
+ time = date_time_or_string
91
+ time = time.to_time if time.is_a?(Date)
92
+ time = Time.parse(time) if time.is_a?(String)
93
+
94
+ time.iso8601
95
+ end
96
+
97
+ def santize_employment_type_query(employment_type)
98
+ # Sökkriterier anställningstyp.
99
+ # Värdena ska ligga mellan 1 och 3.
100
+ # 1 är XXX (EJ DOKUMENTERAT)
101
+ # 2 är somarjobb / feriejobb
102
+ # 3 är utlandsjobb
103
+
104
+ # TODO: The question is what we do if an invalid parameter is passed
105
+ # should we crash?
106
+
107
+ employment_type
108
+ end
109
+
110
+ def santize_keywords_query(keywords)
111
+ #
112
+ # Sökord kan separeras eller kombineras med något av följande exempel:
113
+ # mellanslag (” ”)
114
+ #
115
+ # [Example]
116
+ # /matchning?nyckelord="bagare""test"
117
+ # /matchning?nyckelord="bagare"OR"test" /matchning?nyckelord="automatisk"AND"test"
118
+
119
+ # Valid characters
120
+ # abcdefghijklmnopqrstuvwxyzåäö0123456789: ,.-"
121
+
122
+ # TODO: What do we do if invalid characters are passed? Crash?
123
+ keywords
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,47 @@
1
+ require 'uri'
2
+ require 'net/http'
3
+ require 'json'
4
+
5
+ module Arbetsformedlingen
6
+ module API
7
+ class Request
8
+ Response = KeyStruct.new(:code, :body, :json)
9
+
10
+ HEADERS = {
11
+ 'Content-Type' => 'application/json',
12
+ 'Accept-Language' => 'sv'
13
+ }.freeze
14
+
15
+ attr_reader :locale, :base_url
16
+
17
+ def initialize(base_url: '', locale: 'sv')
18
+ @base_url = base_url
19
+ @locale = locale
20
+ end
21
+
22
+ def get(url, query: {})
23
+ uri = URI("#{base_url}#{url}?#{URI.encode_www_form(query.to_a)}")
24
+
25
+ http = Net::HTTP.new(uri.host, uri.port)
26
+
27
+ request = Net::HTTP::Get.new(uri)
28
+ request['Content-Type'] = HEADERS['Content-Type']
29
+ request['Accept-Language'] = locale
30
+
31
+ response = http.request(request)
32
+
33
+ Response.new(
34
+ code: response.code,
35
+ body: response.read_body,
36
+ json: parse_json(response.read_body)
37
+ )
38
+ end
39
+
40
+ def parse_json(string)
41
+ JSON.parse(string.to_s)
42
+ rescue JSON::ParserError => _e
43
+ {}
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,91 @@
1
+ require 'arbetsformedlingen/api/values/ad_result_values'
2
+
3
+ module Arbetsformedlingen
4
+ module API
5
+ module AdResult
6
+ def self.build(response_data)
7
+ data = response_data.fetch('platsannons')
8
+
9
+ ad_data = data.fetch('annons')
10
+
11
+ Values::Ad.new(
12
+ id: ad_data.fetch('annonsid'),
13
+ url: ad_data.fetch('platsannonsUrl'),
14
+ title: ad_data.fetch('annonsrubrik'),
15
+ body: ad_data.fetch('annonstext'),
16
+ occupation: ad_data.fetch('yrkesbenamning'),
17
+ occupation_id: ad_data.fetch('yrkesid'),
18
+ published_at: ad_data.fetch('publiceraddatum'),
19
+ total_vacancies: ad_data.fetch('antal_platser'),
20
+ municipalities: ad_data.fetch('kommunnamn'),
21
+ municipality_id: ad_data.fetch('kommunkod'),
22
+ total_vacancies_with_visa: ad_data.fetch('antalplatserVisa'),
23
+ employment_type: ad_data.fetch('anstallningstyp'),
24
+ terms: build_terms(data.fetch('villkor')),
25
+ application: build_application(data.fetch('ansokan')),
26
+ workplace: build_workplace(data.fetch('arbetsplats')),
27
+ requirements: build_requirements(data.fetch('krav'))
28
+ )
29
+ end
30
+
31
+ def self.build_terms(data)
32
+ Values::Terms.new(
33
+ duration: data.fetch('varaktighet'),
34
+ working_hours: data.fetch('arbetstid'),
35
+ working_hours_description: data.fetch('arbetstidvaraktighet'),
36
+ salary_type: data.fetch('lonetyp'),
37
+ salary_form: data.fetch('loneform')
38
+ )
39
+ end
40
+
41
+ def self.build_application(data)
42
+ Values::Application.new(
43
+ reference: data.fetch('referens'),
44
+ application_url: data.fetch('webbplats'),
45
+ email: data['epostadress'],
46
+ last_application_at: data.fetch('sista_ansokningsdag'),
47
+ application_comment: data.fetch('ovrigt_om_ansokan')
48
+ )
49
+ end
50
+
51
+ def self.build_workplace(data)
52
+ Values::Workplace.new(
53
+ name: data.fetch('arbetsplatsnamn'),
54
+ postal: build_postal(data),
55
+ country: data.fetch('land'),
56
+ visit_address: data.fetch('besoksadress'),
57
+ logotype_url: data.fetch('logotypurl'),
58
+ website: data.fetch('hemsida'),
59
+ contacts: (
60
+ data.dig('kontaktpersonlista', 'kontaktpersonlista') || []
61
+ ).map do |contact_data|
62
+ build_workplace_contacts(contact_data)
63
+ end
64
+ )
65
+ end
66
+
67
+ def self.build_postal(data)
68
+ Values::Postal.new(
69
+ code: data.fetch('postnummer'),
70
+ address: data.fetch('postadress'),
71
+ city: data.fetch('postort'),
72
+ country: data.fetch('postland')
73
+ )
74
+ end
75
+
76
+ def self.build_workplace_contacts(data)
77
+ Values::Contact.new(
78
+ name: data['namn'],
79
+ title: data['titel'],
80
+ phone: data['telefonnummer']
81
+ )
82
+ end
83
+
84
+ def self.build_requirements(data)
85
+ Values::Requirements.new(
86
+ own_car: data.fetch('egenbil')
87
+ )
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,44 @@
1
+ require 'arbetsformedlingen/api/values/matchning_result_values'
2
+
3
+ module Arbetsformedlingen
4
+ module API
5
+ module MatchningResult
6
+ def self.build(response_data)
7
+ data = response_data.fetch('matchningslista')
8
+
9
+ Values::MatchningPage.new(
10
+ list_name: 'annonser',
11
+ total_ads: data.fetch('antal_platsannonser'),
12
+ total_ads_exact: data.fetch('antal_platsannonser_exakta'),
13
+ total_ads_nearby: data.fetch('antal_platsannonser_narliggande'),
14
+ total_vacancies_on_page: data.fetch('antal_platserTotal'),
15
+ total_pages: data.fetch('antal_sidor'),
16
+ raw_data: response_data,
17
+ data: data.fetch('matchningdata').map { |ad_data| build_ad_result(ad_data) }
18
+ )
19
+ end
20
+
21
+ def self.build_ad_result(ad_data)
22
+ Values::MatchningAd.new(
23
+ id: ad_data.fetch('annonsid'),
24
+ title: ad_data.fetch('annonsrubrik'),
25
+ occupation: ad_data.fetch('yrkesbenamning'),
26
+ occupation_id: ad_data.fetch('yrkesbenamningId'),
27
+ company: ad_data.fetch('arbetsplatsnamn'),
28
+ municipalities: ad_data.fetch('kommunnamn'),
29
+ municipality_id: ad_data.fetch('kommunkod'),
30
+ published_at: ad_data.fetch('publiceraddatum'),
31
+ last_application_at: ad_data.fetch('sista_ansokningsdag'),
32
+ url: ad_data.fetch('annonsurl'),
33
+ relevance: ad_data.fetch('relevans'),
34
+ total_vacancies: ad_data.fetch('antalplatser'),
35
+ total_vacancies_with_visa: ad_data.fetch('antalPlatserVisa'),
36
+ duration_id: ad_data.fetch('varaktighetId'),
37
+ counties: ad_data.fetch('lan'),
38
+ country_id: ad_data.fetch('lanid'),
39
+ employment_type: ad_data.fetch('anstallningstyp')
40
+ )
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,26 @@
1
+ require 'arbetsformedlingen/api/values/soklista_values'
2
+
3
+ module Arbetsformedlingen
4
+ module API
5
+ module SoklistaResult
6
+ def self.build(response_data)
7
+ data = response_data.fetch('soklista')
8
+
9
+ Values::SoklistaPage.new(
10
+ list_name: data.fetch('listnamn'),
11
+ total_ads: data.fetch('totalt_antal_platsannonser'),
12
+ total_vacancies: data.fetch('totalt_antal_ledigajobb'),
13
+ raw_data: response_data,
14
+ data: data.fetch('sokdata').map do |result|
15
+ Values::SoklistaResult.new(
16
+ id: result.fetch('id'),
17
+ name: result.fetch('namn'),
18
+ total_ads: result.fetch('antal_platsannonser'),
19
+ total_vacancies: result.fetch('antal_ledigajobb')
20
+ )
21
+ end
22
+ )
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,70 @@
1
+ module Arbetsformedlingen
2
+ module API
3
+ module Values
4
+ Ad = KeyStruct.new(
5
+ :id,
6
+ :url,
7
+ :title,
8
+ :body,
9
+ :occupation,
10
+ :occupation_id,
11
+ :published_at,
12
+ :total_vacancies,
13
+ :municipalities,
14
+ :municipality_id,
15
+ :total_vacancies_with_visa,
16
+ :employment_type,
17
+ :terms,
18
+ :application,
19
+ :workplace,
20
+ :requirements
21
+ )
22
+ class Ad
23
+ def to_h
24
+ hash = super.to_h
25
+ hash[:terms] = hash[:terms].to_h
26
+ hash[:application] = hash[:application].to_h
27
+ hash[:workplace] = hash[:workplace].to_h
28
+ hash[:requirements] = hash[:requirements].to_h
29
+ hash
30
+ end
31
+ end
32
+
33
+ Terms = KeyStruct.new(
34
+ :duration,
35
+ :working_hours,
36
+ :working_hours_description,
37
+ :salary_type,
38
+ :salary_form
39
+ )
40
+
41
+ Application = KeyStruct.new(
42
+ :reference,
43
+ :application_url,
44
+ :email,
45
+ :last_application_at,
46
+ :application_comment
47
+ )
48
+
49
+ Workplace = KeyStruct.new(
50
+ :name,
51
+ :postal,
52
+ :country,
53
+ :visit_address,
54
+ :logotype_url,
55
+ :website,
56
+ :contacts
57
+ )
58
+ class Workplace
59
+ def to_h
60
+ data = super.to_h
61
+ data[:postal] = data[:postal].to_h
62
+ data
63
+ end
64
+ end
65
+
66
+ Postal = KeyStruct.new(:code, :address, :city, :country)
67
+ Requirements = KeyStruct.new(:own_car)
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,40 @@
1
+ module Arbetsformedlingen
2
+ module API
3
+ module Values
4
+ class CreateAdPage
5
+ ResponseMessage = KeyStruct.new(:detail, :error_code)
6
+
7
+ attr_reader :code, :messages, :body, :request_body
8
+
9
+ def initialize(httparty_response, request_boby)
10
+ @code = httparty_response.code
11
+ @body = httparty_response.body
12
+ @request_body = request_body
13
+ @valid = @code == 202
14
+ @messages = build_messages(httparty_response.to_a)
15
+ end
16
+
17
+ def valid?
18
+ @valid
19
+ end
20
+
21
+ private
22
+
23
+ def build_messages(messages)
24
+ messages.map do |message|
25
+ # HTTParty returns an array if there is only one key-value pair in the response
26
+ # so we need to check for it here and normalize
27
+ if message.is_a?(Array)
28
+ ResponseMessage.new(detail: message.last, error_code: nil)
29
+ else
30
+ error_code = message['ErrorCode']
31
+ @valid = false if error_code
32
+
33
+ ResponseMessage.new(detail: message['Message'], error_code: error_code)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,50 @@
1
+ module Arbetsformedlingen
2
+ module API
3
+ module Values
4
+ MatchningPage = KeyStruct.new(
5
+ :list_name,
6
+ :total_ads,
7
+ :total_ads_exact,
8
+ :total_ads_nearby,
9
+ :total_vacancies_on_page,
10
+ :total_places_total,
11
+ :total_pages,
12
+ :data,
13
+ :raw_data
14
+ )
15
+ class MatchningPage
16
+ include Enumerable
17
+
18
+ def each(&block)
19
+ data.each(&block)
20
+ end
21
+
22
+ def to_h
23
+ hash = super.to_h
24
+ hash[:data].map!(&:to_h)
25
+ hash
26
+ end
27
+ end
28
+
29
+ MatchningAd = KeyStruct.new(
30
+ :id,
31
+ :title,
32
+ :occupation,
33
+ :occupation_id,
34
+ :company,
35
+ :municipalities,
36
+ :municipality_id,
37
+ :published_at,
38
+ :last_application_at,
39
+ :url,
40
+ :relevance,
41
+ :total_vacancies,
42
+ :total_vacancies_with_visa,
43
+ :duration_id,
44
+ :counties,
45
+ :country_id,
46
+ :employment_type
47
+ )
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,33 @@
1
+ module Arbetsformedlingen
2
+ module API
3
+ module Values
4
+ SoklistaPage = KeyStruct.new(
5
+ :list_name,
6
+ :total_ads,
7
+ :total_vacancies,
8
+ :data,
9
+ :raw_data
10
+ )
11
+ class SoklistaPage
12
+ include Enumerable
13
+
14
+ def each(&block)
15
+ data.each(&block)
16
+ end
17
+
18
+ def to_h
19
+ hash = super.to_h
20
+ hash[:data].map!(&:to_h)
21
+ hash
22
+ end
23
+ end
24
+
25
+ SoklistaResult = KeyStruct.new(
26
+ :id,
27
+ :name,
28
+ :total_ads,
29
+ :total_vacancies
30
+ )
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,13 @@
1
+ module Arbetsformedlingen
2
+ class KeyStruct < Struct
3
+ def initialize(**keyword_args)
4
+ keyword_args.each do |key, value|
5
+ if members.include?(key)
6
+ self[key] = value
7
+ else
8
+ raise ArgumentError, "Unknown key struct member: #{key}"
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,3 +1,5 @@
1
+ require 'time'
2
+
1
3
  module Arbetsformedlingen
2
4
  module Predicates
3
5
  include Dry::Logic::Predicates
@@ -1,4 +1,5 @@
1
1
  require 'builder'
2
+ require 'arbetsformedlingen/models/packet_xml_builder'
2
3
 
3
4
  module Arbetsformedlingen
4
5
  PacketSchema = Dry::Validation.Form do
@@ -35,5 +36,9 @@ module Arbetsformedlingen
35
36
  hash[:position] = @position.to_h
36
37
  hash
37
38
  end
39
+
40
+ def to_xml
41
+ PacketXMLBuilder.new(self).to_xml
42
+ end
38
43
  end
39
44
  end
@@ -1,15 +1,17 @@
1
1
  require 'builder'
2
2
 
3
3
  module Arbetsformedlingen
4
- class OutputBuilder
4
+ class PacketXMLBuilder
5
5
  def initialize(packet)
6
6
  @packet = packet
7
7
  end
8
8
 
9
9
  def to_xml
10
- # TODO: Set option so that åäö isn't encoded
11
- builder = Builder::XmlMarkup.new(indent: 2)
12
- append_envelope(builder, @packet.to_h)
10
+ @xml ||= begin
11
+ # TODO: Set option so that åäö isn't encoded
12
+ builder = Builder::XmlMarkup.new(indent: 2)
13
+ append_envelope(builder, @packet.to_h)
14
+ end
13
15
  end
14
16
 
15
17
  private
@@ -1,3 +1,3 @@
1
1
  module Arbetsformedlingen
2
- VERSION = '0.2.0'.freeze
2
+ VERSION = '0.3.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: arbetsformedlingen
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jacob Burenstam
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-09-28 00:00:00.000000000 Z
11
+ date: 2017-10-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: httparty
@@ -94,6 +94,34 @@ dependencies:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
96
  version: '3.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: webmock
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '3.1'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '3.1'
111
+ - !ruby/object:Gem::Dependency
112
+ name: vcr
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '3.0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '3.0'
97
125
  - !ruby/object:Gem::Dependency
98
126
  name: byebug
99
127
  requirement: !ruby/object:Gem::Requirement
@@ -130,13 +158,24 @@ files:
130
158
  - data/municipality-codes.csv
131
159
  - data/occupation-codes.csv
132
160
  - lib/arbetsformedlingen.rb
133
- - lib/arbetsformedlingen/client.rb
161
+ - lib/arbetsformedlingen/api/client.rb
162
+ - lib/arbetsformedlingen/api/ledigtarbete_client.rb
163
+ - lib/arbetsformedlingen/api/matchning_client.rb
164
+ - lib/arbetsformedlingen/api/request.rb
165
+ - lib/arbetsformedlingen/api/results/ad_result.rb
166
+ - lib/arbetsformedlingen/api/results/matchning_result.rb
167
+ - lib/arbetsformedlingen/api/results/soklista_result.rb
168
+ - lib/arbetsformedlingen/api/values/ad_result_values.rb
169
+ - lib/arbetsformedlingen/api/values/create_ad_page.rb
170
+ - lib/arbetsformedlingen/api/values/matchning_result_values.rb
171
+ - lib/arbetsformedlingen/api/values/soklista_values.rb
134
172
  - lib/arbetsformedlingen/codes/country_code.rb
135
173
  - lib/arbetsformedlingen/codes/drivers_license_code.rb
136
174
  - lib/arbetsformedlingen/codes/experience_required_code.rb
137
175
  - lib/arbetsformedlingen/codes/municipality_code.rb
138
176
  - lib/arbetsformedlingen/codes/occupation_code.rb
139
177
  - lib/arbetsformedlingen/codes/salary_type_code.rb
178
+ - lib/arbetsformedlingen/key_struct.rb
140
179
  - lib/arbetsformedlingen/models/application_method.rb
141
180
  - lib/arbetsformedlingen/models/company.rb
142
181
  - lib/arbetsformedlingen/models/document.rb
@@ -144,13 +183,12 @@ files:
144
183
  - lib/arbetsformedlingen/models/dry/types.rb
145
184
  - lib/arbetsformedlingen/models/model.rb
146
185
  - lib/arbetsformedlingen/models/packet.rb
186
+ - lib/arbetsformedlingen/models/packet_xml_builder.rb
147
187
  - lib/arbetsformedlingen/models/position.rb
148
188
  - lib/arbetsformedlingen/models/publication.rb
149
189
  - lib/arbetsformedlingen/models/qualification.rb
150
190
  - lib/arbetsformedlingen/models/salary.rb
151
191
  - lib/arbetsformedlingen/models/schedule.rb
152
- - lib/arbetsformedlingen/output_builder.rb
153
- - lib/arbetsformedlingen/response.rb
154
192
  - lib/arbetsformedlingen/version.rb
155
193
  homepage: https://github.com/buren/arbetsformedlingen
156
194
  licenses:
@@ -1,25 +0,0 @@
1
- require 'arbetsformedlingen/response'
2
-
3
- module Arbetsformedlingen
4
- class Client
5
- BASE_URL = 'http://api.arbetsformedlingen.se/ledigtarbete'.freeze
6
- ROUTES = {
7
- post_job_url: "#{BASE_URL}/apiledigtarbete/hrxml",
8
- test_post_job_url: "#{BASE_URL}/apiledigtarbete/test/hrxml"
9
- }.freeze
10
-
11
- HEADERS = {
12
- 'Content-type' => 'text/xml'
13
- }.freeze
14
-
15
- def self.post_job(xml)
16
- response = HTTParty.post(post_job_url, body: xml, headers: HEADERS)
17
- Response.new(response, xml)
18
- end
19
-
20
- def self.post_job_url
21
- return ROUTES.fetch(:test_post_job_url) if Arbetsformedlingen.config.test
22
- ROUTES.fetch(:post_job_url)
23
- end
24
- end
25
- end
@@ -1,34 +0,0 @@
1
- module Arbetsformedlingen
2
- class Response
3
- attr_reader :code, :messages, :body, :request_body
4
-
5
- def initialize(httparty_response, request_boby)
6
- @code = httparty_response.code
7
- @body = httparty_response.body
8
- @request_body = request_body
9
- @valid = @code == 202
10
- @messages = build_messages(httparty_response.to_a)
11
- end
12
-
13
- def valid?
14
- @valid
15
- end
16
-
17
- private
18
-
19
- def build_messages(messages)
20
- messages.map do |message|
21
- # HTTParty returns an array if there is only one key-value pair in the response
22
- # so we need to check for it here and normalize
23
- if message.is_a?(Array)
24
- { message: message.last, error_code: nil }
25
- else
26
- error_code = message['ErrorCode']
27
- @valid = false if error_code
28
-
29
- { message: message['Message'], error_code: error_code }
30
- end
31
- end
32
- end
33
- end
34
- end