kosapi_client 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/bin/console +15 -0
- data/bin/setup +23 -0
- data/lib/kosapi_client.rb +22 -0
- data/lib/kosapi_client/api_client.rb +43 -0
- data/lib/kosapi_client/configuration.rb +23 -0
- data/lib/kosapi_client/entity.rb +19 -0
- data/lib/kosapi_client/entity/author.rb +17 -0
- data/lib/kosapi_client/entity/base_entity.rb +16 -0
- data/lib/kosapi_client/entity/base_person.rb +20 -0
- data/lib/kosapi_client/entity/boolean.rb +14 -0
- data/lib/kosapi_client/entity/course.rb +34 -0
- data/lib/kosapi_client/entity/course_event.rb +20 -0
- data/lib/kosapi_client/entity/data_mappings.rb +122 -0
- data/lib/kosapi_client/entity/enum.rb +11 -0
- data/lib/kosapi_client/entity/exam.rb +25 -0
- data/lib/kosapi_client/entity/id.rb +13 -0
- data/lib/kosapi_client/entity/link.rb +54 -0
- data/lib/kosapi_client/entity/ml_string.rb +42 -0
- data/lib/kosapi_client/entity/parallel.rb +20 -0
- data/lib/kosapi_client/entity/person.rb +10 -0
- data/lib/kosapi_client/entity/result_page.rb +46 -0
- data/lib/kosapi_client/entity/student.rb +25 -0
- data/lib/kosapi_client/entity/teacher.rb +14 -0
- data/lib/kosapi_client/entity/teacher_timetable_slot.rb +16 -0
- data/lib/kosapi_client/entity/timetable_slot.rb +16 -0
- data/lib/kosapi_client/hash_utils.rb +17 -0
- data/lib/kosapi_client/http_client.rb +36 -0
- data/lib/kosapi_client/kosapi_client.rb +45 -0
- data/lib/kosapi_client/kosapi_response.rb +32 -0
- data/lib/kosapi_client/oauth2_http_adapter.rb +36 -0
- data/lib/kosapi_client/request_builder.rb +59 -0
- data/lib/kosapi_client/request_builder_delegator.rb +52 -0
- data/lib/kosapi_client/resource.rb +12 -0
- data/lib/kosapi_client/resource/course_events_builder.rb +13 -0
- data/lib/kosapi_client/resource/courses_builder.rb +12 -0
- data/lib/kosapi_client/resource/exams_builder.rb +13 -0
- data/lib/kosapi_client/resource/parallels_builder.rb +20 -0
- data/lib/kosapi_client/resource/teachers_builder.rb +16 -0
- data/lib/kosapi_client/resource_mapper.rb +20 -0
- data/lib/kosapi_client/response_converter.rb +65 -0
- data/lib/kosapi_client/response_links.rb +40 -0
- data/lib/kosapi_client/response_preprocessor.rb +48 -0
- data/lib/kosapi_client/url_builder.rb +24 -0
- data/lib/kosapi_client/version.rb +3 -0
- data/spec/integration/course_events_spec.rb +13 -0
- data/spec/integration/courses_spec.rb +28 -0
- data/spec/integration/exams_spec.rb +20 -0
- data/spec/integration/parallels_spec.rb +68 -0
- data/spec/kosapi_client/api_client_spec.rb +22 -0
- data/spec/kosapi_client/configuration_spec.rb +30 -0
- data/spec/kosapi_client/entity/base_entity_spec.rb +20 -0
- data/spec/kosapi_client/entity/base_person_spec.rb +19 -0
- data/spec/kosapi_client/entity/boolean_spec.rb +23 -0
- data/spec/kosapi_client/entity/course_event_spec.rb +30 -0
- data/spec/kosapi_client/entity/data_mappings_spec.rb +154 -0
- data/spec/kosapi_client/entity/enum_spec.rb +20 -0
- data/spec/kosapi_client/entity/id_spec.rb +13 -0
- data/spec/kosapi_client/entity/link_spec.rb +89 -0
- data/spec/kosapi_client/entity/ml_string_spec.rb +52 -0
- data/spec/kosapi_client/entity/parallel_spec.rb +18 -0
- data/spec/kosapi_client/entity/result_page_spec.rb +42 -0
- data/spec/kosapi_client/entity/teacher_timetable_slot_spec.rb +19 -0
- data/spec/kosapi_client/entity/timetable_slot_spec.rb +22 -0
- data/spec/kosapi_client/hash_utils_spec.rb +20 -0
- data/spec/kosapi_client/http_client_spec.rb +30 -0
- data/spec/kosapi_client/kosapi_client_spec.rb +57 -0
- data/spec/kosapi_client/kosapi_response_spec.rb +96 -0
- data/spec/kosapi_client/oauth2_http_adapter_spec.rb +25 -0
- data/spec/kosapi_client/request_builder_delegator_spec.rb +72 -0
- data/spec/kosapi_client/request_builder_spec.rb +80 -0
- data/spec/kosapi_client/resource/courses_builder_spec.rb +20 -0
- data/spec/kosapi_client/resource/parallels_builder_spec.rb +49 -0
- data/spec/kosapi_client/resource_mapper_spec.rb +33 -0
- data/spec/kosapi_client/response_converter_spec.rb +58 -0
- data/spec/kosapi_client/response_links_spec.rb +52 -0
- data/spec/kosapi_client/response_preprocessor_spec.rb +51 -0
- data/spec/kosapi_client/url_builder_spec.rb +44 -0
- data/spec/spec_helper.rb +40 -0
- data/spec/support/client_helpers.rb +11 -0
- data/spec/support/helpers.rb +7 -0
- data/spec/support/shared_examples_for_fluent_api.rb +7 -0
- metadata +401 -0
@@ -0,0 +1,52 @@
|
|
1
|
+
module KOSapiClient
|
2
|
+
class RequestBuilderDelegator
|
3
|
+
|
4
|
+
def initialize(request_builder)
|
5
|
+
@request_builder = request_builder
|
6
|
+
@response = nil
|
7
|
+
end
|
8
|
+
|
9
|
+
alias :super_method_missing :method_missing
|
10
|
+
|
11
|
+
def method_missing(method, *args, &block)
|
12
|
+
if @response
|
13
|
+
delegate_to_response(method, *args, &block)
|
14
|
+
else
|
15
|
+
delegate_to_builder(method, *args, &block)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def respond_to_missing?(method, include_all)
|
20
|
+
if @response
|
21
|
+
@response.respond_to?(method, include_all)
|
22
|
+
else
|
23
|
+
@request_builder.respond_to?(method, include_all)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
def delegate_to_response(method, *args, &block)
|
29
|
+
if @response.respond_to?(method)
|
30
|
+
@response.send(method, *args, &block)
|
31
|
+
else
|
32
|
+
super_method_missing(method, *args)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def delegate_to_builder(method, *args, &block)
|
37
|
+
if @request_builder.respond_to?(method)
|
38
|
+
res = @request_builder.send(method, *args, &block)
|
39
|
+
if res.equal?(@request_builder)
|
40
|
+
self
|
41
|
+
else
|
42
|
+
res
|
43
|
+
end
|
44
|
+
else
|
45
|
+
@request_builder.finalize
|
46
|
+
@response = @request_builder.response
|
47
|
+
delegate_to_response(method, *args, &block)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'kosapi_client/request_builder'
|
2
|
+
require 'kosapi_client/resource/courses_builder'
|
3
|
+
require 'kosapi_client/resource/parallels_builder'
|
4
|
+
require 'kosapi_client/resource/exams_builder'
|
5
|
+
require 'kosapi_client/resource/course_events_builder'
|
6
|
+
require 'kosapi_client/resource/teachers_builder'
|
7
|
+
|
8
|
+
module KOSapiClient
|
9
|
+
module Resource
|
10
|
+
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module KOSapiClient
|
2
|
+
module Resource
|
3
|
+
class CourseEventsBuilder < RequestBuilder
|
4
|
+
|
5
|
+
def attendees
|
6
|
+
raise 'Call #find({course_event_id}) before asking for attendees' unless id_set?
|
7
|
+
url_builder.set_path(id, 'attendees')
|
8
|
+
self
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module KOSapiClient
|
2
|
+
module Resource
|
3
|
+
class ParallelsBuilder < RequestBuilder
|
4
|
+
|
5
|
+
def related
|
6
|
+
raise 'Call #find before asking for related parallels' unless id_set?
|
7
|
+
url_builder.set_path(id, 'related')
|
8
|
+
self
|
9
|
+
end
|
10
|
+
|
11
|
+
def students
|
12
|
+
raise 'Call #find before asking for students' unless id_set?
|
13
|
+
url_builder.set_path(id, 'students')
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module KOSapiClient
|
2
|
+
module Resource
|
3
|
+
class TeachersBuilder < RequestBuilder
|
4
|
+
|
5
|
+
# XXX: This is quite insane, we need some support to DRY subresources.
|
6
|
+
%w[courses parallels exams timetable].each do |resource|
|
7
|
+
define_method(resource) do |semester: 'current'|
|
8
|
+
raise "Call #find({username}) before asking for #{resource}" unless id_set?
|
9
|
+
url_builder.set_path(id, resource)
|
10
|
+
url_builder.set_query_param(:sem, semester)
|
11
|
+
self
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module KOSapiClient
|
2
|
+
module ResourceMapper
|
3
|
+
|
4
|
+
def self.included(base)
|
5
|
+
base.extend(ClassMethods)
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
|
10
|
+
def resource(name)
|
11
|
+
define_method name do
|
12
|
+
builder = create_builder name
|
13
|
+
RequestBuilderDelegator.new(builder)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
using Corefines::String::camelcase
|
2
|
+
|
3
|
+
module KOSapiClient
|
4
|
+
|
5
|
+
# This class converts parsed response in hash format
|
6
|
+
# (wrapped in Response) into domain Ruby objects.
|
7
|
+
# Root domain object type is
|
8
|
+
# determined at runtime based on API response.
|
9
|
+
|
10
|
+
class ResponseConverter
|
11
|
+
|
12
|
+
def initialize(client)
|
13
|
+
@client = client
|
14
|
+
end
|
15
|
+
|
16
|
+
def convert(response)
|
17
|
+
if response.is_paginated?
|
18
|
+
convert_paginated(response)
|
19
|
+
else
|
20
|
+
convert_single(response.item)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns processed entries converted into domain objects
|
25
|
+
# wrapped into ResultPage class instance.
|
26
|
+
# @param response [KOSapiResponse] Response object wrapping array of hashes corresponding to entries
|
27
|
+
# @return [ResultPage] ResultPage of domain objects
|
28
|
+
|
29
|
+
def convert_paginated(response)
|
30
|
+
items = response.items || []
|
31
|
+
converted_items = items.map{ |p| convert_single(p) }
|
32
|
+
Entity::ResultPage.new(converted_items, create_links(response))
|
33
|
+
end
|
34
|
+
|
35
|
+
def convert_single(item)
|
36
|
+
type = detect_type(item)
|
37
|
+
convert_type(item, type)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
def convert_type(hash, type)
|
42
|
+
type.parse(hash)
|
43
|
+
end
|
44
|
+
|
45
|
+
def detect_type(hash)
|
46
|
+
type_str = hash[:xsi_type]
|
47
|
+
extract_type(type_str)
|
48
|
+
end
|
49
|
+
|
50
|
+
def extract_type(type_str)
|
51
|
+
type_name = type_str.camelcase(:upper)
|
52
|
+
begin
|
53
|
+
entity_type = Entity.const_get(type_name)
|
54
|
+
rescue
|
55
|
+
raise "Unknown entity type: #{type_name}"
|
56
|
+
end
|
57
|
+
entity_type
|
58
|
+
end
|
59
|
+
|
60
|
+
def create_links(response)
|
61
|
+
ResponseLinks.parse(response.links_hash, @client)
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'kosapi_client/entity/link'
|
2
|
+
|
3
|
+
module KOSapiClient
|
4
|
+
class ResponseLinks
|
5
|
+
|
6
|
+
attr_reader :prev, :next
|
7
|
+
|
8
|
+
def initialize(prev_link, next_link)
|
9
|
+
@prev = prev_link
|
10
|
+
@next = next_link
|
11
|
+
end
|
12
|
+
|
13
|
+
class << self
|
14
|
+
|
15
|
+
def parse(hash, client)
|
16
|
+
prev_link = parse_link(hash, 'prev', client)
|
17
|
+
next_link = parse_link(hash, 'next', client)
|
18
|
+
new(prev_link, next_link)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
def parse_link(hash, rel, client)
|
23
|
+
return nil unless hash
|
24
|
+
link_hash = extract_link_hash(hash, rel)
|
25
|
+
if link_hash
|
26
|
+
link = Entity::Link.parse(link_hash)
|
27
|
+
link.inject_client(client)
|
28
|
+
link
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def extract_link_hash(hash, rel)
|
33
|
+
hash = [hash] unless hash.instance_of?(Array)
|
34
|
+
hash.find { |it| it[:rel] == rel }
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
using Corefines::String::snake_case
|
2
|
+
|
3
|
+
module KOSapiClient
|
4
|
+
class ResponsePreprocessor
|
5
|
+
|
6
|
+
def preprocess(result)
|
7
|
+
response = extract_parsed(result)
|
8
|
+
result = stringify_keys(response)
|
9
|
+
entries_to_array(result)
|
10
|
+
merge_contents(result)
|
11
|
+
result
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
private
|
16
|
+
def extract_parsed(result)
|
17
|
+
parsed_contents = result.parsed
|
18
|
+
raise 'Wrong type of parsed response. HTTP response body is probably invalid or incomplete.' unless parsed_contents.instance_of?(Hash)
|
19
|
+
parsed_contents
|
20
|
+
end
|
21
|
+
|
22
|
+
def stringify_keys(response)
|
23
|
+
HashUtils.deep_transform_hash_keys(response) { |key| key.snake_case.sub(':', '_').to_sym rescue key }
|
24
|
+
end
|
25
|
+
|
26
|
+
def entries_to_array(hash)
|
27
|
+
if hash[:atom_feed] && hash[:atom_feed][:atom_entry].instance_of?(Hash)
|
28
|
+
hash[:atom_feed][:atom_entry] = [hash[:atom_feed][:atom_entry]]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def merge_contents(hash)
|
33
|
+
entries = if hash[:atom_feed]
|
34
|
+
hash[:atom_feed][:atom_entry]
|
35
|
+
else
|
36
|
+
[hash[:atom_entry]]
|
37
|
+
end
|
38
|
+
if entries
|
39
|
+
entries.each do |entry|
|
40
|
+
content = entry.delete(:atom_content)
|
41
|
+
entry.merge! content if content
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module KOSapiClient
|
2
|
+
class URLBuilder
|
3
|
+
|
4
|
+
def initialize(root_url)
|
5
|
+
@root_url = root_url
|
6
|
+
@template = URITemplate.new(root_url + '{/segments*}{?query*}')
|
7
|
+
@segments = []
|
8
|
+
@query = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def set_path(*segments)
|
12
|
+
@segments = segments
|
13
|
+
end
|
14
|
+
|
15
|
+
def set_query_param(param, value)
|
16
|
+
@query[param] = value
|
17
|
+
end
|
18
|
+
|
19
|
+
def url
|
20
|
+
@template.expand(segments: @segments, query: @query)
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Course events resource', :vcr, :integration do
|
4
|
+
subject(:client) { create_kosapi_client }
|
5
|
+
|
6
|
+
it 'returns course events' do
|
7
|
+
expect(client.course_events.offset(0).limit(10).count).to be 10
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'returns course event attendees' do
|
11
|
+
expect(client.course_events.find(220200484405).attendees.count).to be > 0
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Courses resource', :vcr, :integration do
|
4
|
+
let(:credentials) { { client_id: ENV['KOSAPI_OAUTH_CLIENT_ID'], client_secret: ENV['KOSAPI_OAUTH_CLIENT_SECRET'] } }
|
5
|
+
subject(:client) { KOSapiClient.new(credentials) }
|
6
|
+
|
7
|
+
|
8
|
+
it 'downloads courses data' do
|
9
|
+
page = client.courses.offset(0).limit(15).query(department: '18*')
|
10
|
+
expect(page.items.count).to eq 15
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'provides courses data details' do
|
14
|
+
page = client.courses.offset(20).query(department: '18*', completion: 'EXAM').detail
|
15
|
+
expect(page.items.last.description).not_to be_nil
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'parses entry title' do
|
19
|
+
page = client.courses
|
20
|
+
expect(page.items.first.title).not_to be_nil
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'parses empty response' do
|
24
|
+
page = client.courses.offset(1000000)
|
25
|
+
expect(page.items).to eq []
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Exams resource', :vcr, :integration do
|
4
|
+
|
5
|
+
subject(:client) { create_kosapi_client }
|
6
|
+
|
7
|
+
it 'fetches exams' do
|
8
|
+
expect(client.exams.offset(0).limit(10).count).to be > 0
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'fetches exam students' do
|
12
|
+
expect(client.exams.find(193156727405).attendees.count).to be > 0
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'allows multiple examiners' do
|
16
|
+
exam = client.exams.find(613664749005)
|
17
|
+
expect(exam.examiners.count).to be 2
|
18
|
+
expect(exam.examiners.first.link_id).to eq 'zhoufjar'
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Parallels resource', :vcr, :integration do
|
4
|
+
|
5
|
+
let(:credentials) { { client_id: ENV['KOSAPI_OAUTH_CLIENT_ID'], client_secret: ENV['KOSAPI_OAUTH_CLIENT_SECRET'] } }
|
6
|
+
subject(:client) { KOSapiClient.new(credentials) }
|
7
|
+
|
8
|
+
it 'fetches parallels list' do
|
9
|
+
page = client.parallels.offset(0).limit(50).query('course.department' => '18*')
|
10
|
+
expect(page.items.count).to eq 50
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'parses relationship links properly' do
|
14
|
+
page = client.parallels
|
15
|
+
course = page.items.first.course
|
16
|
+
expect(course.link_title).not_to be_nil
|
17
|
+
expect(course.link_href).not_to be_nil
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'parses entry ID properly' do
|
21
|
+
page = client.parallels
|
22
|
+
parallel = page.items.first
|
23
|
+
expect(parallel.id).not_to be_nil
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'parses updated properly' do
|
27
|
+
page = client.parallels
|
28
|
+
parallel = page.items.first
|
29
|
+
expect(parallel.updated).not_to be_nil
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'parses author properly' do
|
33
|
+
page = client.parallels
|
34
|
+
parallel = page.items.first
|
35
|
+
expect(parallel.author.name).not_to be_nil
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'parses entry link properly' do
|
39
|
+
page = client.parallels
|
40
|
+
parallel = page.items.first
|
41
|
+
expect(parallel.link).not_to be_nil
|
42
|
+
expect(parallel.link.link_href).not_to be_nil
|
43
|
+
expect(parallel.link.link_rel).not_to be_nil
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'parses timetable slot ID' do
|
47
|
+
page = client.parallels
|
48
|
+
slot = page.items.first.timetable_slots.first
|
49
|
+
expect(slot.id).not_to be_nil
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'returns following result page with next callback' do
|
53
|
+
page = client.parallels
|
54
|
+
following_page = page.next
|
55
|
+
expect(following_page.items.count).to be > 0
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'follows next link with RSQL query' do
|
59
|
+
page = client.parallels.where('(lastUpdatedDate>=2014-07-01T00:00:00;lastUpdatedDate<=2014-07-10T00:00:00)')
|
60
|
+
expect(page.next.items.count).to be > 0
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'returns students for a parallel' do
|
64
|
+
students = client.parallels.find(339540000).students.limit(20)
|
65
|
+
expect(students.count).to eq 15
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|