arbetsformedlingen 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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