kenpo_api 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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b82bbf5c88a60e118f141f57fba80bbddb6a3faf
4
+ data.tar.gz: 9c32cfa6e1cf75caeeee598b1ca8487e799b6ad3
5
+ SHA512:
6
+ metadata.gz: 77d4c099e64c304931ac0c19621f5f7332114e2dce0590f803017b01ebbaec8085bfc4e07c4e560c82a5f6fd2ad8430ca889a2dee16b1b4d45e985c47285dc75
7
+ data.tar.gz: 01541a96c10f03919640366f21ff0aa23f74e07a158c35e6f3cdb9bb86a49b4342f3376456fc970b9e5924d563a287b520468026ea11b08b127467dd681e96f0
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1,7 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.3
5
+ - 2.2.6
6
+ - 2.1.10
7
+ before_install: gem install bundler -v 1.13.6
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in kenpo_api.gemspec
4
+ gemspec
@@ -0,0 +1,86 @@
1
+ # KenpoApi
2
+
3
+ Ruby binding for kenpo reservation API ([関東ITソフトウェア健康保険組合 施設・レクリエーション](https://as.its-kenpo.or.jp/)).
4
+
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'kenpo_api'
12
+ ```
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install kenpo_api
21
+
22
+ ## Usage
23
+
24
+ ```ruby
25
+ # Resort reservation
26
+ resort_names = KenpoApi::Resort.resort_names
27
+ KenpoApi::Resort.request_reservation_url(resort_name: 'トスラブ箱根和奏林', email: 'matsuno_osomatsu@example.com')
28
+ url = 'https://as.its-kenpo.or.jp/apply/new?c=aaaaaaaa-bbbb-cccc-dddd-012345678901'
29
+ criteria = KenpoApi::Resort.check_reservation_criteria(url)
30
+ reservation_data = {
31
+ sign_no: 6666,
32
+ insured_no: 666,
33
+ office_name: '株式会社FLAG',
34
+ kana_name: 'マツノ オソマツ',
35
+ birth_year: 1962,
36
+ birth_month: 5,
37
+ birth_day: 24,
38
+ gender: :man,
39
+ relationship: :myself,
40
+ contact_phone: '666-6666-6666',
41
+ postal_code: '180-0004',
42
+ state: 13,
43
+ address: '武蔵野市本町666-666',
44
+ join_time: '2017-04-15',
45
+ night_count: 1,
46
+ stay_persons: 6,
47
+ room_persons: 6, (1 room) # or [3, 3] (2 rooms) or [2, 2, 2] (3 rooms) ...
48
+ meeting_dates: nil, # (none) or [1] (first day only) or [1, 2] (both days) ...
49
+ must_meeting: false,
50
+ }
51
+ KenpoApi::Resort.apply_reservation(url, reservation_data)
52
+
53
+ # Sport reservation
54
+ sport_names = KenpoApi::Sport.sport_names
55
+ KenpoApi::Sport.request_reservation_url(sport_name: 'サマディ門前仲町', email: 'matsuno_osomatsu@example.com')
56
+ url = 'https://as.its-kenpo.or.jp/apply/new?c=aaaaaaaa-bbbb-cccc-dddd-901234567890'
57
+ criteria = KenpoApi::Sport.check_reservation_criteria(url)
58
+ reservation_data = {
59
+ sign_no: 6666,
60
+ insured_no: 666,
61
+ office_name: '株式会社FLAG',
62
+ kana_name: 'マツノ オソマツ',
63
+ birth_year: 1962,
64
+ birth_month: 5,
65
+ birth_day: 24,
66
+ contact_phone: '666-6666-6666',
67
+ postal_code: '180-0004',
68
+ state: 13,
69
+ address: '武蔵野市本町666-666',
70
+ join_time: '2017-03-11',
71
+ use_time_from: '13:00',
72
+ use_time_to: '15:00',
73
+ }
74
+ KenpoApi::Sport.apply_reservation(url, reservation_data)
75
+ ```
76
+
77
+ ## Development
78
+
79
+ 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.
80
+
81
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
82
+
83
+ ## Contributing
84
+
85
+ Bug reports and pull requests are welcome on GitHub at https://github.com/tearoom6/kenpo_api.
86
+
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "kenpo_api"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,39 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'kenpo_api/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'kenpo_api'
8
+ spec.version = KenpoApi::VERSION
9
+ spec.authors = ['tearoom6']
10
+ spec.email = ['tearoom6.biz@gmail.com']
11
+
12
+ spec.summary = %q{Unofficial API for ITS kenpo reservation system.}
13
+ spec.description = %q{Unofficial API for ITS kenpo reservation system.}
14
+ spec.homepage = 'https://github.com/tearoom6/kenpo_api'
15
+
16
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
17
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
18
+ if spec.respond_to?(:metadata)
19
+ #spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'"
20
+ else
21
+ raise 'RubyGems 2.0 or newer is required to protect against ' \
22
+ 'public gem pushes.'
23
+ end
24
+
25
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
26
+ f.match(%r{^(test|spec|features)/})
27
+ end
28
+ spec.bindir = 'exe'
29
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
30
+ spec.require_paths = ['lib']
31
+
32
+ spec.add_development_dependency 'bundler', '~> 1.13'
33
+ spec.add_development_dependency 'rake', '~> 10.0'
34
+ spec.add_development_dependency 'rspec', '~> 3.0'
35
+ spec.add_development_dependency 'faraday', '~> 0.10.0'
36
+ spec.add_development_dependency 'cookiejar', '~> 0.3.3'
37
+ spec.add_development_dependency 'nokogiri', '~> 1.6'
38
+ spec.add_development_dependency 'dry-validation', '~> 0.10.5'
39
+ end
@@ -0,0 +1,18 @@
1
+ require 'kenpo_api/version'
2
+ require 'kenpo_api/client'
3
+ require 'kenpo_api/service_category'
4
+ require 'kenpo_api/service_group'
5
+ require 'kenpo_api/service'
6
+ require 'kenpo_api/routines'
7
+ require 'kenpo_api/resort'
8
+ require 'kenpo_api/sport'
9
+
10
+ module KenpoApi
11
+ class KenpoApiError < StandardError; end
12
+ class NetworkError < KenpoApiError; end
13
+ class NotFoundError < KenpoApiError; end
14
+ class NotAvailableError < KenpoApiError; end
15
+ class ParseError < KenpoApiError; end
16
+ class ValidationError < KenpoApiError; end
17
+
18
+ end
@@ -0,0 +1,72 @@
1
+ require 'singleton'
2
+ require 'faraday'
3
+ require 'cookiejar'
4
+ require 'nokogiri'
5
+
6
+ module KenpoApi
7
+ class Client
8
+ include Singleton
9
+
10
+ BASE_URL = 'https://as.its-kenpo.or.jp/'
11
+
12
+ attr_accessor :timeout, :open_timeout
13
+
14
+ def initialize
15
+ @conn = Faraday.new(url: BASE_URL) do |builder|
16
+ builder.use Faraday::Request::UrlEncoded
17
+ builder.adapter Faraday.default_adapter
18
+ end
19
+ # Set default settings.
20
+ @timeout = 5
21
+ @open_timeout = 5
22
+ @cookiejar = CookieJar::Jar.new
23
+ end
24
+
25
+ def access(path:, method: :get, params: {})
26
+ response = @conn.send(method, path, params) do |req|
27
+ req.options.timeout = @timeout
28
+ req.options.open_timeout = @open_timeout
29
+ req.headers['cookie'] = @cookiejar.get_cookie_header(@conn.url_prefix.to_s)
30
+ end
31
+ raise NetworkError.new("Failed to fetch http content. path: #{path} status_code: #{response.status}") unless response.success?
32
+
33
+ @cookiejar.set_cookie(@conn.url_prefix.to_s, response.headers['set-cookie']) if response.headers.has_key?('set-cookie')
34
+
35
+ return (yield response) if block_given?
36
+ response
37
+ rescue KenpoApiError => e
38
+ raise e
39
+ rescue => e
40
+ raise NetworkError.new("Failed to fetch http content. path: #{path} original_error: #{e.message}")
41
+ end
42
+
43
+ def fetch_document(path:, method: :get, params: {})
44
+ response = access(path: path, method: method, params: params)
45
+ document = Nokogiri::HTML(response.body)
46
+
47
+ return (yield document) if block_given?
48
+ document
49
+ end
50
+
51
+ def parse_single_form_page(path:, method: :get, params: {})
52
+ document = fetch_document(path: path, method: method, params: params)
53
+ form_element = document.xpath('//form').first
54
+
55
+ next_page_info = nil
56
+ unless form_element.nil?
57
+ path = form_element['action']
58
+ method = form_element['method']
59
+ params = document.xpath('//input[@type="hidden"]').select {|input| ! input['name'].nil?}.map {|input| [input['name'], input['value']]}.to_h
60
+ next_page_info = {path: path, method: method, params: params}
61
+ end
62
+
63
+ return (yield next_page_info, document) if block_given?
64
+ return next_page_info, document
65
+ rescue KenpoApiError => e
66
+ raise e
67
+ rescue => e
68
+ raise ParseError.new("Failed to parse HTML. message: #{e.message}")
69
+ end
70
+
71
+ end
72
+ end
@@ -0,0 +1,130 @@
1
+ require 'dry-validation'
2
+
3
+ module KenpoApi
4
+ class Resort
5
+ extend Routines
6
+
7
+ def self.resort_names
8
+ category = ServiceCategory.find(:resort_reserve)
9
+ raise NotFoundError.new("Service category not found. code: #{category_code}") if category.nil?
10
+ category.service_groups.map {|group| group.name}
11
+ end
12
+
13
+ def self.request_reservation_url(resort_name:, email:)
14
+ service = find_service(category_code: :resort_reserve, group_name: resort_name)
15
+ request_application_url(service_path: service.path, email: email)
16
+ end
17
+
18
+ def self.check_reservation_criteria(application_url)
19
+ html_document = Client.instance.fetch_document(path: application_url)
20
+ raise NotAvailableError.new("Application URL is invalid: #{html_document.xpath('//p').first.content}") if html_document.xpath('//form').first.nil?
21
+ reservation_criteria(html_document)
22
+ end
23
+
24
+ def self.apply_reservation(application_url, application_data)
25
+ apply(application_url: application_url) do |html_document|
26
+ reservation_data = self.validate_reservation_data(application_data, html_document)
27
+ convert_to_reservation_post_params(reservation_data)
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def self.reservation_criteria(html_document)
34
+ criteria = {}
35
+ criteria[:note] = html_document.xpath('//div[@class="note mb10"]').first.text
36
+ criteria[:service_name] = html_document.xpath('//form/div[@class="form_box"]//dd[@class="elements"]').first.text
37
+ criteria[:birth_year] = html_document.xpath('id("apply_year")/*/@value') .map {|attr| attr.value }.select {|val| val != '' } # (1917..2017)
38
+ criteria[:birth_month] = html_document.xpath('id("apply_month")/*/@value') .map {|attr| attr.value }.select {|val| val != '' } # (1..12)
39
+ criteria[:birth_day] = html_document.xpath('id("apply_day")/*/@value') .map {|attr| attr.value }.select {|val| val != '' } # (1..31)
40
+ criteria[:gender] = html_document.xpath('id("apply_gender")/*/@value') .map {|attr| attr.value }.map {|val| val.to_sym } # [:man, :woman]
41
+ criteria[:relationship] = html_document.xpath('id("apply_relationship")/*/@value').map {|attr| attr.value }.map {|val| val.to_sym } # [:myself, :family]
42
+ criteria[:state] = html_document.xpath('id("apply_state")/*/@value') .map {|attr| attr.value }.select {|val| val != '' } # (1..47)
43
+ criteria[:join_time] = html_document.xpath('id("apply_join_time")/*/@value') .map {|attr| attr.value }.select {|val| val != '' } # ['2017-04-01', .., '2017-04-30']
44
+ criteria[:night_count] = html_document.xpath('id("apply_night_count")/*/@value') .map {|attr| attr.value }.select {|val| val != '' } # (1..2)
45
+ criteria[:room_number] = html_document.xpath('id("house_select")/*/@value') .map {|attr| attr.value.to_i } # (1..10)
46
+ criteria
47
+ end
48
+
49
+ def self.preprocess_reservation_data(reservation_data)
50
+ reservation_data[:birth_year] = reservation_data[:birth_year].to_s
51
+ reservation_data[:birth_month] = reservation_data[:birth_month].to_s
52
+ reservation_data[:birth_day] = reservation_data[:birth_day].to_s
53
+ reservation_data[:state] = reservation_data[:state].to_s
54
+ reservation_data[:night_count] = reservation_data[:night_count].to_s
55
+ reservation_data[:room_persons] = Array(reservation_data[:room_persons])
56
+ reservation_data[:meeting_dates] = Array(reservation_data[:meeting_dates])
57
+ reservation_data
58
+ end
59
+
60
+ def self.validate_reservation_data(reservation_data, html_document)
61
+ reservation_data = preprocess_reservation_data(reservation_data)
62
+ criteria = reservation_criteria(html_document)
63
+
64
+ schema = Dry::Validation.Schema do
65
+ required(:sign_no) .filled(:int?)
66
+ required(:insured_no) .filled(:int?)
67
+ required(:office_name) .filled(:str?)
68
+ required(:kana_name) .filled(:str?)
69
+ required(:birth_year) .filled(included_in?: criteria[:birth_year])
70
+ required(:birth_month) .filled(included_in?: criteria[:birth_month])
71
+ required(:birth_day) .filled(included_in?: criteria[:birth_day])
72
+ required(:gender) .filled(included_in?: criteria[:gender])
73
+ required(:relationship) .filled(included_in?: criteria[:relationship])
74
+ required(:contact_phone).filled(format?: /^[0-9-]+$/)
75
+ required(:postal_code) .filled(format?: /^[0-9]{3}-[0-9]{4}$/)
76
+ required(:state) .filled(included_in?: criteria[:state])
77
+ required(:address) .filled(:str?)
78
+ required(:join_time) .filled(included_in?: criteria[:join_time])
79
+ required(:night_count) .filled(included_in?: criteria[:night_count])
80
+ required(:stay_persons) .filled(:int?)
81
+ required(:room_persons) .filled{ array? & each(:int?) & size?(criteria[:room_number]) }
82
+ required(:meeting_dates).value{ array? & each{ int? & included_in?([1,2,3]) } & size?((0..3)) }
83
+ required(:must_meeting) .maybe(:bool?)
84
+ end
85
+
86
+ result = schema.call(reservation_data)
87
+ raise ValidationError.new("Reservation data is invalid. #{result.messages.to_s}") if result.failure?
88
+ reise ValidationError.new('Stay persons count should match the sum of room persons') unless reservation_data[:stay_persons] != reservation_data[:room_persons].inject(:+)
89
+ result.output
90
+ end
91
+
92
+ def self.convert_to_reservation_post_params(original_data)
93
+ post_params = {}
94
+ post_params['apply[sign_no]'] = original_data[:sign_no]
95
+ post_params['apply[insured_no]'] = original_data[:insured_no]
96
+ post_params['apply[office_name]'] = original_data[:office_name]
97
+ post_params['apply[kana_name]'] = original_data[:kana_name]
98
+ post_params['apply[year]'] = original_data[:birth_year]
99
+ post_params['apply[month]'] = original_data[:birth_month]
100
+ post_params['apply[day]'] = original_data[:birth_day]
101
+ post_params['apply[gender]'] = original_data[:gender]
102
+ post_params['apply[relationship]'] = original_data[:relationship]
103
+ post_params['apply[contact_phone]'] = original_data[:contact_phone]
104
+ post_params['apply[postal]'] = original_data[:postal_code]
105
+ post_params['apply[state]'] = original_data[:state]
106
+ post_params['apply[address]'] = original_data[:address]
107
+ post_params['apply[join_time]'] = original_data[:join_time]
108
+ post_params['apply[night_count]'] = original_data[:night_count]
109
+ post_params['apply[stay_persons]'] = original_data[:stay_persons]
110
+ post_params['apply[hope_rooms]'] = original_data[:room_persons].size()
111
+ post_params['apply[hope_room1]'] = original_data[:room_persons][0]
112
+ post_params['apply[hope_room2]'] = original_data[:room_persons][1] || ''
113
+ post_params['apply[hope_room3]'] = original_data[:room_persons][2] || ''
114
+ post_params['apply[hope_room4]'] = original_data[:room_persons][3] || ''
115
+ post_params['apply[hope_room5]'] = original_data[:room_persons][4] || ''
116
+ post_params['apply[hope_room6]'] = original_data[:room_persons][5] || ''
117
+ post_params['apply[hope_room7]'] = original_data[:room_persons][6] || ''
118
+ post_params['apply[hope_room8]'] = original_data[:room_persons][7] || ''
119
+ post_params['apply[hope_room9]'] = original_data[:room_persons][8] || ''
120
+ post_params['apply[hope_room10]'] = original_data[:room_persons][9] || ''
121
+ post_params['apply[use_meeting_flag]'] = original_data[:meeting_dates].any? ? 'use' : 'no_use'
122
+ post_params['apply[use_meeting1]'] = original_data[:meeting_dates].include?(1) ? '1' : '0'
123
+ post_params['apply[use_meeting2]'] = original_data[:meeting_dates].include?(2) ? '1' : '0'
124
+ post_params['apply[use_meeting3]'] = (original_data[:meeting_dates].include?(3) && original_data[:night_count] >= 2) ? '1' : '0'
125
+ post_params['apply[must_meeting]'] = original_data[:must_meeting] ? 'must' : 'not_must'
126
+ post_params
127
+ end
128
+
129
+ end
130
+ end
@@ -0,0 +1,45 @@
1
+ module KenpoApi
2
+ module Routines
3
+
4
+ # Returns first service if you specify nil to service_name.
5
+ def find_service(category_code:, group_name:, service_name: nil)
6
+ category = ServiceCategory.find(category_code.to_sym)
7
+ raise NotFoundError.new("Service category not found. code: #{category_code}") if category.nil?
8
+
9
+ group = category.find_service_group(group_name)
10
+ raise NotFoundError.new("Service group not found. name: #{group_name}") if group.nil?
11
+ raise NotAvailableError.new("No available services. name: #{group_name}") unless group.available?
12
+
13
+ return group.services.first if service_name.nil?
14
+ service = group.find_service(service_name)
15
+ raise NotFoundError.new("Service not found. name: #{service_name}") if service.nil?
16
+ service
17
+ end
18
+
19
+ def request_application_url(service_path:, email:)
20
+ # Accept agreement.
21
+ next_page_info, = Client.instance.parse_single_form_page(path: service_path)
22
+
23
+ # Input email.
24
+ next_page_info, = Client.instance.parse_single_form_page(next_page_info)
25
+ next_page_info[:params]['email'] = email
26
+
27
+ Client.instance.access(next_page_info)
28
+ end
29
+
30
+ # Block param receives one argument (html_document), and should return additional POST params.
31
+ def apply(application_url:)
32
+ # Input application form.
33
+ next_page_info, html_document = Client.instance.parse_single_form_page(path: application_url)
34
+ raise NotAvailableError.new("Application URL is invalid: #{html_document.xpath('//p').first.content}") if next_page_info.nil?
35
+
36
+ next_page_info[:params].merge!(yield html_document) if block_given?
37
+
38
+ # Confirm.
39
+ next_page_info, = Client.instance.parse_single_form_page(next_page_info)
40
+
41
+ Client.instance.access(next_page_info)
42
+ end
43
+
44
+ end
45
+ end
@@ -0,0 +1,27 @@
1
+ module KenpoApi
2
+ class Service
3
+ attr_reader :group, :name, :path
4
+
5
+ def initialize(group:, name:, path:)
6
+ @group = group
7
+ @name = name
8
+ @path = path
9
+ end
10
+
11
+ def self.list(service_group)
12
+ return [] if service_group.nil?
13
+ Client.instance.fetch_document(path: service_group.path).xpath('//section[@class="request-box"]//a').map do |link|
14
+ self.new(
15
+ group: service_group,
16
+ name: link.text,
17
+ path: link['href'],
18
+ )
19
+ end
20
+ end
21
+
22
+ def self.find(service_group, name)
23
+ self.list(service_group).find { |service| service.name == name }
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,51 @@
1
+ module KenpoApi
2
+ class ServiceCategory
3
+ CATEGORIES = {
4
+ resort_reserve: '直営・通年・夏季保養施設(抽選申込)',
5
+ resort_search_vacant: '直営・通年・夏季保養施設(空き照会)',
6
+ sport_reserve: 'スポーツ施設(抽選申込)',
7
+ sport_search_vacant: 'スポーツ施設(空き照会)',
8
+ resort_alliance: '契約保養施設(補助金対象施設)',
9
+ travel_pack: 'ITS旅行パック(補助申請)',
10
+ golf_course: 'ITS契約ゴルフ場',
11
+ camp_site: 'ITS契約オートキャンプ場',
12
+ laforet: 'ラフォーレ倶楽部',
13
+ thalassotherapy: 'タラソテラピー',
14
+ recreation: '体育奨励イベント',
15
+ }
16
+
17
+ attr_reader :category_code, :name, :path
18
+
19
+ def initialize(name:, path:)
20
+ @category_code = CATEGORIES.key(name)
21
+ @name = name
22
+ @path = path
23
+ end
24
+
25
+ def self.list
26
+ Client.instance.fetch_document(path: '/service_category/index').xpath('//div[@class="request-box"]//a').map do |link|
27
+ self.new(
28
+ name: link.text,
29
+ path: link['href'],
30
+ )
31
+ end
32
+ end
33
+
34
+ def self.find(category_code)
35
+ self.list.find { |category| category.category_code == category_code }
36
+ end
37
+
38
+ def service_groups
39
+ ServiceGroup.list(self)
40
+ end
41
+
42
+ def find_service_group(name)
43
+ ServiceGroup.find(self, name)
44
+ end
45
+
46
+ def available?
47
+ self.service_groups.any?
48
+ end
49
+
50
+ end
51
+ end
@@ -0,0 +1,39 @@
1
+ module KenpoApi
2
+ class ServiceGroup
3
+ attr_reader :category, :name, :path
4
+
5
+ def initialize(category:, name:, path:)
6
+ @category = category
7
+ @name = name
8
+ @path = path
9
+ end
10
+
11
+ def self.list(service_category)
12
+ return [] if service_category.nil?
13
+ Client.instance.fetch_document(path: service_category.path).xpath('//section[@class="request-box"]//a').map do |link|
14
+ self.new(
15
+ category: service_category,
16
+ name: link.text,
17
+ path: link['href'],
18
+ )
19
+ end
20
+ end
21
+
22
+ def self.find(service_category, name)
23
+ self.list(service_category).find { |group| group.name == name }
24
+ end
25
+
26
+ def services
27
+ Service.list(self)
28
+ end
29
+
30
+ def find_service(name)
31
+ Service.find(self, name)
32
+ end
33
+
34
+ def available?
35
+ self.services.any?
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,101 @@
1
+ require 'dry-validation'
2
+
3
+ module KenpoApi
4
+ class Sport
5
+ extend Routines
6
+
7
+ def self.sport_names
8
+ category = ServiceCategory.find(:sport_reserve)
9
+ raise NotFoundError.new("Service category not found. code: #{category_code}") if category.nil?
10
+ category.service_groups.map {|group| group.name}
11
+ end
12
+
13
+ def self.request_reservation_url(sport_name:, email:)
14
+ service = find_service(category_code: :sport_reserve, group_name: sport_name)
15
+ request_application_url(service_path: service.path, email: email)
16
+ end
17
+
18
+ def self.check_reservation_criteria(application_url)
19
+ html_document = Client.instance.fetch_document(path: application_url)
20
+ raise NotAvailableError.new("Application URL is invalid: #{html_document.xpath('//p').first.content}") if html_document.xpath('//form').first.nil?
21
+ reservation_criteria(html_document)
22
+ end
23
+
24
+ def self.apply_reservation(application_url, application_data)
25
+ apply(application_url: application_url) do |html_document|
26
+ reservation_data = self.validate_reservation_data(application_data, html_document)
27
+ convert_to_reservation_post_params(reservation_data)
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def self.reservation_criteria(html_document)
34
+ criteria = {}
35
+ criteria[:note] = html_document.xpath('//div[@class="note mb10"]').first.text
36
+ criteria[:service_name] = html_document.xpath('//form/div[@class="form_box"]//dd[@class="elements"]').first.text
37
+ criteria[:birth_year] = html_document.xpath('id("apply_year")/*/@value') .map {|attr| attr.value }.select {|val| val != '' } # (1917..2017)
38
+ criteria[:birth_month] = html_document.xpath('id("apply_month")/*/@value') .map {|attr| attr.value }.select {|val| val != '' } # (1..12)
39
+ criteria[:birth_day] = html_document.xpath('id("apply_day")/*/@value') .map {|attr| attr.value }.select {|val| val != '' } # (1..31)
40
+ criteria[:state] = html_document.xpath('id("apply_state")/*/@value') .map {|attr| attr.value }.select {|val| val != '' } # (1..47)
41
+ criteria[:join_time] = html_document.xpath('id("apply_join_time")/*/@value') .map {|attr| attr.value }.select {|val| val != '' } # ['2017-04-01', .., '2017-04-30'] (only Saterdays or Sundays?)
42
+ criteria[:use_time_from] = html_document.xpath('id("apply_use_time_from")/*/@value').map {|attr| attr.value }.select {|val| val != '' } # ['00:00', .., '24:00']
43
+ criteria[:use_time_to] = html_document.xpath('id("apply_use_time_to")/*/@value') .map {|attr| attr.value }.select {|val| val != '' } # ['00:00', .., '24:00']
44
+ criteria
45
+ end
46
+
47
+ def self.preprocess_reservation_data(reservation_data)
48
+ reservation_data[:birth_year] = reservation_data[:birth_year].to_s
49
+ reservation_data[:birth_month] = reservation_data[:birth_month].to_s
50
+ reservation_data[:birth_day] = reservation_data[:birth_day].to_s
51
+ reservation_data[:state] = reservation_data[:state].to_s
52
+ reservation_data
53
+ end
54
+
55
+ def self.validate_reservation_data(reservation_data, html_document)
56
+ reservation_data = preprocess_reservation_data(reservation_data)
57
+ criteria = reservation_criteria(html_document)
58
+
59
+ schema = Dry::Validation.Schema do
60
+ required(:sign_no) .filled(:int?)
61
+ required(:insured_no) .filled(:int?)
62
+ required(:office_name) .filled(:str?)
63
+ required(:kana_name) .filled(:str?)
64
+ required(:birth_year) .filled(included_in?: criteria[:birth_year])
65
+ required(:birth_month) .filled(included_in?: criteria[:birth_month])
66
+ required(:birth_day) .filled(included_in?: criteria[:birth_day])
67
+ required(:contact_phone).filled(format?: /^[0-9-]+$/)
68
+ required(:postal_code) .filled(format?: /^[0-9]{3}-[0-9]{4}$/)
69
+ required(:state) .filled(included_in?: criteria[:state])
70
+ required(:address) .filled(:str?)
71
+ required(:join_time) .filled(included_in?: criteria[:join_time])
72
+ required(:use_time_from).filled(included_in?: criteria[:use_time_from])
73
+ required(:use_time_to) .filled(included_in?: criteria[:use_time_to])
74
+ end
75
+
76
+ result = schema.call(reservation_data)
77
+ raise ValidationError.new("Reservation data is invalid. #{result.messages.to_s}") if result.failure?
78
+ result.output
79
+ end
80
+
81
+ def self.convert_to_reservation_post_params(original_data)
82
+ post_params = {}
83
+ post_params['apply[sign_no]'] = original_data[:sign_no]
84
+ post_params['apply[insured_no]'] = original_data[:insured_no]
85
+ post_params['apply[office_name]'] = original_data[:office_name]
86
+ post_params['apply[kana_name]'] = original_data[:kana_name]
87
+ post_params['apply[year]'] = original_data[:birth_year]
88
+ post_params['apply[month]'] = original_data[:birth_month]
89
+ post_params['apply[day]'] = original_data[:birth_day]
90
+ post_params['apply[contact_phone]'] = original_data[:contact_phone]
91
+ post_params['apply[postal]'] = original_data[:postal_code]
92
+ post_params['apply[state]'] = original_data[:state]
93
+ post_params['apply[address]'] = original_data[:address]
94
+ post_params['apply[join_time]'] = original_data[:join_time]
95
+ post_params['apply[use_time_from]'] = original_data[:use_time_from]
96
+ post_params['apply[use_time_to]'] = original_data[:use_time_to]
97
+ post_params
98
+ end
99
+
100
+ end
101
+ end
@@ -0,0 +1,3 @@
1
+ module KenpoApi
2
+ VERSION = '0.1.0'
3
+ end
metadata ADDED
@@ -0,0 +1,159 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kenpo_api
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - tearoom6
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-02-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.13'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.13'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: faraday
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 0.10.0
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 0.10.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: cookiejar
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 0.3.3
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 0.3.3
83
+ - !ruby/object:Gem::Dependency
84
+ name: nokogiri
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.6'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.6'
97
+ - !ruby/object:Gem::Dependency
98
+ name: dry-validation
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 0.10.5
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 0.10.5
111
+ description: Unofficial API for ITS kenpo reservation system.
112
+ email:
113
+ - tearoom6.biz@gmail.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - ".gitignore"
119
+ - ".rspec"
120
+ - ".travis.yml"
121
+ - Gemfile
122
+ - README.md
123
+ - Rakefile
124
+ - bin/console
125
+ - bin/setup
126
+ - kenpo_api.gemspec
127
+ - lib/kenpo_api.rb
128
+ - lib/kenpo_api/client.rb
129
+ - lib/kenpo_api/resort.rb
130
+ - lib/kenpo_api/routines.rb
131
+ - lib/kenpo_api/service.rb
132
+ - lib/kenpo_api/service_category.rb
133
+ - lib/kenpo_api/service_group.rb
134
+ - lib/kenpo_api/sport.rb
135
+ - lib/kenpo_api/version.rb
136
+ homepage: https://github.com/tearoom6/kenpo_api
137
+ licenses: []
138
+ metadata: {}
139
+ post_install_message:
140
+ rdoc_options: []
141
+ require_paths:
142
+ - lib
143
+ required_ruby_version: !ruby/object:Gem::Requirement
144
+ requirements:
145
+ - - ">="
146
+ - !ruby/object:Gem::Version
147
+ version: '0'
148
+ required_rubygems_version: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ requirements: []
154
+ rubyforge_project:
155
+ rubygems_version: 2.5.2
156
+ signing_key:
157
+ specification_version: 4
158
+ summary: Unofficial API for ITS kenpo reservation system.
159
+ test_files: []