kenpo_api 0.1.0

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