arbetsformedlingen 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +5 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +138 -0
  8. data/Rakefile +6 -0
  9. data/arbetsformedlingen.gemspec +33 -0
  10. data/bin/console +14 -0
  11. data/bin/setup +8 -0
  12. data/config/locales/errors.yml +16 -0
  13. data/data/country-codes.csv +237 -0
  14. data/data/municipality-codes.csv +292 -0
  15. data/lib/arbetsformedlingen.rb +56 -0
  16. data/lib/arbetsformedlingen/client.rb +25 -0
  17. data/lib/arbetsformedlingen/codes/country_code.rb +24 -0
  18. data/lib/arbetsformedlingen/codes/drivers_license_code.rb +33 -0
  19. data/lib/arbetsformedlingen/codes/experience_required_code.rb +21 -0
  20. data/lib/arbetsformedlingen/codes/municipality_code.rb +31 -0
  21. data/lib/arbetsformedlingen/codes/salary_type_code.rb +21 -0
  22. data/lib/arbetsformedlingen/models/application_method.rb +20 -0
  23. data/lib/arbetsformedlingen/models/company.rb +50 -0
  24. data/lib/arbetsformedlingen/models/document.rb +28 -0
  25. data/lib/arbetsformedlingen/models/dry/predicates.rb +66 -0
  26. data/lib/arbetsformedlingen/models/dry/types.rb +34 -0
  27. data/lib/arbetsformedlingen/models/model.rb +25 -0
  28. data/lib/arbetsformedlingen/models/packet.rb +39 -0
  29. data/lib/arbetsformedlingen/models/position.rb +56 -0
  30. data/lib/arbetsformedlingen/models/publication.rb +30 -0
  31. data/lib/arbetsformedlingen/models/qualification.rb +23 -0
  32. data/lib/arbetsformedlingen/models/salary.rb +20 -0
  33. data/lib/arbetsformedlingen/models/schedule.rb +56 -0
  34. data/lib/arbetsformedlingen/output_builder.rb +226 -0
  35. data/lib/arbetsformedlingen/response.rb +34 -0
  36. data/lib/arbetsformedlingen/version.rb +3 -0
  37. metadata +178 -0
@@ -0,0 +1,31 @@
1
+ require 'csv'
2
+
3
+ module Arbetsformedlingen
4
+ class MunicipalityCode
5
+ CODE_MAP = CSV.read(
6
+ File.expand_path('../../../../data/municipality-codes.csv', __FILE__)
7
+ ).to_h.freeze
8
+ CODES_MAP_INVERTED = CODE_MAP.invert.freeze
9
+
10
+ def self.to_code(name)
11
+ normalized = normalize(name)
12
+ CODE_MAP.fetch(normalized) do
13
+ normalized if CODES_MAP_INVERTED[normalized]
14
+ end
15
+ end
16
+
17
+ def self.valid?(name)
18
+ !to_code(name).nil?
19
+ end
20
+
21
+ def self.normalize(name)
22
+ name.to_s.strip
23
+ end
24
+
25
+ def self.to_form_array(name_only: false)
26
+ return CODE_MAP.to_a unless name_only
27
+
28
+ CODE_MAP.map { |name, _code| [name, name] }
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,21 @@
1
+ require 'set'
2
+
3
+ module Arbetsformedlingen
4
+ class SalaryTypeCode
5
+ CODE_MAP = {
6
+ 'fixed' => '1',
7
+ 'fixed_and_commission' => '2',
8
+ 'commission' => '3'
9
+ }.freeze
10
+
11
+ CODES = Set.new(CODE_MAP.values).freeze
12
+
13
+ def self.to_code(value)
14
+ CODE_MAP.fetch(value.to_s, nil)
15
+ end
16
+
17
+ def self.valid?(value)
18
+ CODES.include?(value.to_s)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,20 @@
1
+ module Arbetsformedlingen
2
+ ApplicationMethodSchema = Dry::Validation.Form do
3
+ configure do
4
+ config.type_specs = true
5
+ config.messages_file = File.expand_path('../../../../config/locales/errors.yml', __FILE__)
6
+
7
+ predicates(Predicates)
8
+ end
9
+
10
+ required(:external, Types::Bool).filled
11
+ required(:summary, Types::StrippedString).filled
12
+ required(:url, Types::StrippedString).filled(:url?)
13
+ end
14
+
15
+ class ApplicationMethod < Model
16
+ def initialize(hash)
17
+ super(ApplicationMethodSchema.call(hash))
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arbetsformedlingen
4
+ CompanySchema = Dry::Validation.Form do
5
+ configure do
6
+ config.type_specs = true
7
+ config.messages_file = File.expand_path('../../../../config/locales/errors.yml', __FILE__)
8
+
9
+ predicates(Predicates)
10
+ end
11
+
12
+ required(:name, Types::StrippedString).filled
13
+ required(:cin, Types::CIN).filled(:str?, :cin?)
14
+ required(:description, Types::StrippedString).filled
15
+
16
+ required(:address).schema do
17
+ required(:municipality, Types::Municipality).filled(:municipality?)
18
+ required(:country_code, Types::Country).filled(:str?, :country_code?)
19
+ required(:street, Types::StrippedString).filled
20
+ required(:city, Types::StrippedString).filled
21
+ required(:zip, Types::Zip).filled(:str?, :zip?)
22
+ end
23
+ end
24
+
25
+ class Company < Model
26
+ def initialize(hash)
27
+ super(CompanySchema.call(hash))
28
+ end
29
+
30
+ def to_h
31
+ hash = super
32
+ address = hash.fetch(:address)
33
+ hash[:address][:full_address] = [
34
+ address.fetch(:street),
35
+ address.fetch(:zip),
36
+ address.fetch(:city)
37
+ ].join(', ')
38
+ hash[:cin_arbetsformedlingen] = cin_arbetsformedlingen(hash.fetch(:cin))
39
+ hash
40
+ end
41
+
42
+ # Formats a Company Identification Number the way Arbetsformedlingen likes it
43
+ def cin_arbetsformedlingen(cin)
44
+ String.new(cin.dup).
45
+ delete('-').
46
+ insert(6, '-').
47
+ insert(0, '46-')
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,28 @@
1
+ require 'securerandom'
2
+
3
+ module Arbetsformedlingen
4
+ DocumentSchema = Dry::Validation.Form do
5
+ configure do
6
+ config.type_specs = true
7
+ config.messages_file = File.expand_path('../../../../config/locales/errors.yml', __FILE__)
8
+
9
+ predicates(Predicates)
10
+ end
11
+
12
+ required(:id, Types::UUIDString).filled
13
+ required(:timestamp, :string).filled(:iso8601_date?)
14
+ required(:customer_id, Types::StrippedString).filled
15
+ required(:email, Types::StrippedString).filled(:str?, :email?)
16
+ end
17
+
18
+ class Document < Model
19
+ def initialize(hash)
20
+ data = {
21
+ id: SecureRandom.uuid,
22
+ timestamp: Time.now.utc.iso8601
23
+ }.merge!(hash)
24
+
25
+ super(DocumentSchema.call(data))
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,66 @@
1
+ module Arbetsformedlingen
2
+ module Predicates
3
+ include Dry::Logic::Predicates
4
+
5
+ predicate(:iso8601_date?) do |value|
6
+ begin
7
+ Time.iso8601(value)
8
+ rescue ArgumentError => _e
9
+ false
10
+ end
11
+ end
12
+
13
+ predicate(:yyyy_mm_dd?) do |value|
14
+ begin
15
+ Date.strptime(value, '%Y-%m-%d')
16
+ rescue ArgumentError => _e
17
+ false
18
+ end
19
+ end
20
+
21
+ predicate(:email?) do |value|
22
+ value.include?('@') && value.include?('.')
23
+ end
24
+
25
+ predicate(:currency?) do |value|
26
+ value.length == 3
27
+ end
28
+
29
+ predicate(:zip?) do |value|
30
+ value.length == 5
31
+ end
32
+
33
+ predicate(:cin?) do |value|
34
+ value.length == 10
35
+ end
36
+
37
+ predicate(:municipality?) do |value|
38
+ MunicipalityCode.valid?(value)
39
+ end
40
+
41
+ predicate(:salary_type?) do |value|
42
+ SalaryTypeCode.valid?(value)
43
+ end
44
+
45
+ predicate(:experience_required?) do |value|
46
+ ExperienceRequiredCode.valid?(value)
47
+ end
48
+
49
+ predicate(:country_code?) do |value|
50
+ CountryCode.valid?(value)
51
+ end
52
+
53
+ predicate(:drivers_license?) do |value|
54
+ DriversLicenseCode.valid?(value)
55
+ end
56
+
57
+ predicate(:ssyk_id?) do |value|
58
+ # TODO: Add proper validation
59
+ value
60
+ end
61
+
62
+ predicate(:url?) do |value|
63
+ value.include?('https://') || value.include?('http://')
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,34 @@
1
+ module Arbetsformedlingen
2
+ module Types
3
+ include Dry::Types.module
4
+
5
+ StrippedString = Types::Strict::String.constructor { |value| value.to_s.strip }
6
+ UUIDString = Types::Strict::String.constrained(size: 36)
7
+ Currency = Types::Strict::String.constructor do |string|
8
+ string.strip.upcase if string
9
+ end
10
+ # Company Identification Number
11
+ CIN = Types::Strict::String.constructor do |string|
12
+ string.delete(' ').delete('-') if string
13
+ end
14
+ Zip = Types::Strict::String.constructor do |string|
15
+ string.delete(' ') if string
16
+ end
17
+ Municipality = Types::Strict::String.constructor do |string|
18
+ MunicipalityCode.to_code(string)
19
+ end
20
+ Country = Types::Strict::String.constructor do |string|
21
+ CountryCode.to_code(string)
22
+ end
23
+ PositionDuration = Types::Strict::Int
24
+ DriversLicense = Types::Strict::String.constructor do |value|
25
+ DriversLicenseCode.to_code(value)
26
+ end
27
+ SalaryType = Types::Strict::String.constructor do |value|
28
+ SalaryTypeCode.to_code(value)
29
+ end
30
+ ExperienceRequired = Types::Strict::String.constructor do |value|
31
+ ExperienceRequiredCode.to_code(value)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,25 @@
1
+ module Arbetsformedlingen
2
+ class Model
3
+ def self.from_schema(schema)
4
+ new(schema)
5
+ end
6
+
7
+ attr_reader :schema
8
+
9
+ def initialize(schema)
10
+ @schema = schema
11
+ end
12
+
13
+ def valid?
14
+ @schema.success?
15
+ end
16
+
17
+ def errors
18
+ @schema.errors
19
+ end
20
+
21
+ def to_h
22
+ @schema.to_h
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,39 @@
1
+ require 'builder'
2
+
3
+ module Arbetsformedlingen
4
+ PacketSchema = Dry::Validation.Form do
5
+ configure do
6
+ config.type_specs = true
7
+ config.messages_file = File.expand_path('../../../../config/locales/errors.yml', __FILE__)
8
+
9
+ predicates(Predicates)
10
+ end
11
+
12
+ required(:active, Types::Bool).filled
13
+ required(:job_id, Types::StrippedString).filled
14
+ required(:id, Types::StrippedString).filled
15
+ required(:number_to_fill, Types::Int).filled(gt?: 0)
16
+ required(:ssyk_id, Types::Int).filled(:ssyk_id?)
17
+ end
18
+
19
+ class Packet < Model
20
+ DEFAULT_PACKET_ID = 1
21
+
22
+ def initialize(attributes:, publication:, document:, position:)
23
+ hash = attributes
24
+ @publication = publication
25
+ @document = document
26
+ @position = position
27
+ id = hash.fetch(:id, DEFAULT_PACKET_ID)
28
+ super(PacketSchema.call(hash.merge(id: id)))
29
+ end
30
+
31
+ def to_h
32
+ hash = super
33
+ hash[:publication] = @publication.to_h
34
+ hash[:document] = @document.to_h
35
+ hash[:position] = @position.to_h
36
+ hash
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,56 @@
1
+ module Arbetsformedlingen
2
+ PositionSchema = Dry::Validation.Form do
3
+ configure do
4
+ config.type_specs = true
5
+ config.messages_file = File.expand_path('../../../../config/locales/errors.yml', __FILE__)
6
+
7
+ predicates(Predicates)
8
+ end
9
+
10
+ required(:title, Types::StrippedString).filled
11
+ required(:purpose, Types::StrippedString).filled
12
+ required(:address).schema do
13
+ required(:municipality, Types::Municipality).filled(:municipality?)
14
+ required(:country_code, Types::Country).filled(:str?, :country_code?)
15
+
16
+ optional(:street, Types::StrippedString).filled
17
+ optional(:city, Types::StrippedString).filled
18
+ optional(:zip, Types::Zip).filled(:str?, :zip?)
19
+ end
20
+ end
21
+
22
+ class Position < Model
23
+ def initialize(attributes:, company:, schedule:, salary:, qualifications:, application_method:)
24
+ hash = attributes
25
+ @company = company
26
+ @schedule = schedule
27
+ @salary = salary
28
+ @qualifications = qualifications
29
+ @application_method = application_method
30
+
31
+ super(PositionSchema.call(hash))
32
+ end
33
+
34
+ def to_h
35
+ hash = super
36
+ hash[:company] = @company.to_h
37
+ hash[:schedule] = @schedule.to_h
38
+ hash[:salary] = @salary.to_h
39
+ hash[:qualifications] = @qualifications.map(&:to_h)
40
+ hash[:application_method] = @application_method.to_h
41
+ full_address = build_full_address(hash.fetch(:address))
42
+ hash[:address][:full_address] = full_address if full_address
43
+ hash
44
+ end
45
+
46
+ def build_full_address(address)
47
+ return unless address.key?(:street) || address.key?(:zip) || address.key?(:city)
48
+
49
+ [
50
+ address.fetch(:street),
51
+ address.fetch(:zip),
52
+ address[:city]
53
+ ].compact.join(', ')
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,30 @@
1
+ module Arbetsformedlingen
2
+ PublicationSchema = Dry::Validation.Form do
3
+ configure do
4
+ config.type_specs = true
5
+ config.messages_file = File.expand_path('../../../../config/locales/errors.yml', __FILE__)
6
+
7
+ predicates(Predicates)
8
+ end
9
+
10
+ required(:unpublish_at, :string).filled(:yyyy_mm_dd?)
11
+ required(:name, Types::StrippedString).filled
12
+ required(:email, Types::StrippedString).filled(:str?, :email?)
13
+
14
+ optional(:publish_at, :string).filled(:yyyy_mm_dd?)
15
+ end
16
+
17
+ class Publication < Model
18
+ def initialize(hash)
19
+ data = hash.dup
20
+ publish_date = data[:publish_at] || Time.now.utc
21
+
22
+ data[:publish_at] = publish_date.strftime('%Y-%m-%d')
23
+ data[:unpublish_at] = data[:unpublish_at]&.strftime('%Y-%m-%d')
24
+
25
+ # TODO: Validate that unpublish_at - publish_at is not greater that 180 days
26
+
27
+ super(PublicationSchema.call(data))
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,23 @@
1
+ module Arbetsformedlingen
2
+ QualificationSchema = Dry::Validation.Form do
3
+ configure do
4
+ config.type_specs = true
5
+ config.messages_file = File.expand_path('../../../../config/locales/errors.yml', __FILE__)
6
+
7
+ predicates(Predicates)
8
+ end
9
+
10
+ required(:required, Types::Bool).filled
11
+
12
+ optional(:drivers_license, Types::DriversLicense).filled(:drivers_license?)
13
+ optional(:car, Types::Bool).filled(:bool?)
14
+ optional(:summary, Types::StrippedString).filled
15
+ optional(:experience, Types::ExperienceRequired).filled(:experience_required?)
16
+ end
17
+
18
+ class Qualification < Model
19
+ def initialize(hash)
20
+ super(QualificationSchema.call(hash))
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,20 @@
1
+ module Arbetsformedlingen
2
+ SalarySchema = Dry::Validation.Form do
3
+ configure do
4
+ config.type_specs = true
5
+ config.messages_file = File.expand_path('../../../../config/locales/errors.yml', __FILE__)
6
+
7
+ predicates(Predicates)
8
+ end
9
+
10
+ required(:summary, Types::StrippedString).filled
11
+ required(:currency, Types::Currency).filled(:str?, :currency?)
12
+ required(:type, Types::SalaryType).filled(:str?, :salary_type?)
13
+ end
14
+
15
+ class Salary < Model
16
+ def initialize(hash)
17
+ super(SalarySchema.call(hash))
18
+ end
19
+ end
20
+ end