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.
Files changed (83) hide show
  1. checksums.yaml +7 -0
  2. data/bin/console +15 -0
  3. data/bin/setup +23 -0
  4. data/lib/kosapi_client.rb +22 -0
  5. data/lib/kosapi_client/api_client.rb +43 -0
  6. data/lib/kosapi_client/configuration.rb +23 -0
  7. data/lib/kosapi_client/entity.rb +19 -0
  8. data/lib/kosapi_client/entity/author.rb +17 -0
  9. data/lib/kosapi_client/entity/base_entity.rb +16 -0
  10. data/lib/kosapi_client/entity/base_person.rb +20 -0
  11. data/lib/kosapi_client/entity/boolean.rb +14 -0
  12. data/lib/kosapi_client/entity/course.rb +34 -0
  13. data/lib/kosapi_client/entity/course_event.rb +20 -0
  14. data/lib/kosapi_client/entity/data_mappings.rb +122 -0
  15. data/lib/kosapi_client/entity/enum.rb +11 -0
  16. data/lib/kosapi_client/entity/exam.rb +25 -0
  17. data/lib/kosapi_client/entity/id.rb +13 -0
  18. data/lib/kosapi_client/entity/link.rb +54 -0
  19. data/lib/kosapi_client/entity/ml_string.rb +42 -0
  20. data/lib/kosapi_client/entity/parallel.rb +20 -0
  21. data/lib/kosapi_client/entity/person.rb +10 -0
  22. data/lib/kosapi_client/entity/result_page.rb +46 -0
  23. data/lib/kosapi_client/entity/student.rb +25 -0
  24. data/lib/kosapi_client/entity/teacher.rb +14 -0
  25. data/lib/kosapi_client/entity/teacher_timetable_slot.rb +16 -0
  26. data/lib/kosapi_client/entity/timetable_slot.rb +16 -0
  27. data/lib/kosapi_client/hash_utils.rb +17 -0
  28. data/lib/kosapi_client/http_client.rb +36 -0
  29. data/lib/kosapi_client/kosapi_client.rb +45 -0
  30. data/lib/kosapi_client/kosapi_response.rb +32 -0
  31. data/lib/kosapi_client/oauth2_http_adapter.rb +36 -0
  32. data/lib/kosapi_client/request_builder.rb +59 -0
  33. data/lib/kosapi_client/request_builder_delegator.rb +52 -0
  34. data/lib/kosapi_client/resource.rb +12 -0
  35. data/lib/kosapi_client/resource/course_events_builder.rb +13 -0
  36. data/lib/kosapi_client/resource/courses_builder.rb +12 -0
  37. data/lib/kosapi_client/resource/exams_builder.rb +13 -0
  38. data/lib/kosapi_client/resource/parallels_builder.rb +20 -0
  39. data/lib/kosapi_client/resource/teachers_builder.rb +16 -0
  40. data/lib/kosapi_client/resource_mapper.rb +20 -0
  41. data/lib/kosapi_client/response_converter.rb +65 -0
  42. data/lib/kosapi_client/response_links.rb +40 -0
  43. data/lib/kosapi_client/response_preprocessor.rb +48 -0
  44. data/lib/kosapi_client/url_builder.rb +24 -0
  45. data/lib/kosapi_client/version.rb +3 -0
  46. data/spec/integration/course_events_spec.rb +13 -0
  47. data/spec/integration/courses_spec.rb +28 -0
  48. data/spec/integration/exams_spec.rb +20 -0
  49. data/spec/integration/parallels_spec.rb +68 -0
  50. data/spec/kosapi_client/api_client_spec.rb +22 -0
  51. data/spec/kosapi_client/configuration_spec.rb +30 -0
  52. data/spec/kosapi_client/entity/base_entity_spec.rb +20 -0
  53. data/spec/kosapi_client/entity/base_person_spec.rb +19 -0
  54. data/spec/kosapi_client/entity/boolean_spec.rb +23 -0
  55. data/spec/kosapi_client/entity/course_event_spec.rb +30 -0
  56. data/spec/kosapi_client/entity/data_mappings_spec.rb +154 -0
  57. data/spec/kosapi_client/entity/enum_spec.rb +20 -0
  58. data/spec/kosapi_client/entity/id_spec.rb +13 -0
  59. data/spec/kosapi_client/entity/link_spec.rb +89 -0
  60. data/spec/kosapi_client/entity/ml_string_spec.rb +52 -0
  61. data/spec/kosapi_client/entity/parallel_spec.rb +18 -0
  62. data/spec/kosapi_client/entity/result_page_spec.rb +42 -0
  63. data/spec/kosapi_client/entity/teacher_timetable_slot_spec.rb +19 -0
  64. data/spec/kosapi_client/entity/timetable_slot_spec.rb +22 -0
  65. data/spec/kosapi_client/hash_utils_spec.rb +20 -0
  66. data/spec/kosapi_client/http_client_spec.rb +30 -0
  67. data/spec/kosapi_client/kosapi_client_spec.rb +57 -0
  68. data/spec/kosapi_client/kosapi_response_spec.rb +96 -0
  69. data/spec/kosapi_client/oauth2_http_adapter_spec.rb +25 -0
  70. data/spec/kosapi_client/request_builder_delegator_spec.rb +72 -0
  71. data/spec/kosapi_client/request_builder_spec.rb +80 -0
  72. data/spec/kosapi_client/resource/courses_builder_spec.rb +20 -0
  73. data/spec/kosapi_client/resource/parallels_builder_spec.rb +49 -0
  74. data/spec/kosapi_client/resource_mapper_spec.rb +33 -0
  75. data/spec/kosapi_client/response_converter_spec.rb +58 -0
  76. data/spec/kosapi_client/response_links_spec.rb +52 -0
  77. data/spec/kosapi_client/response_preprocessor_spec.rb +51 -0
  78. data/spec/kosapi_client/url_builder_spec.rb +44 -0
  79. data/spec/spec_helper.rb +40 -0
  80. data/spec/support/client_helpers.rb +11 -0
  81. data/spec/support/helpers.rb +7 -0
  82. data/spec/support/shared_examples_for_fluent_api.rb +7 -0
  83. 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,12 @@
1
+ module KOSapiClient
2
+ module Resource
3
+ class CoursesBuilder < RequestBuilder
4
+
5
+ def detail(level = 1)
6
+ @url_builder.set_query_param(:detail, level)
7
+ self
8
+ end
9
+
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,13 @@
1
+ module KOSapiClient
2
+ module Resource
3
+ class ExamsBuilder < RequestBuilder
4
+
5
+ def attendees
6
+ raise 'Call #find({exam_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,3 @@
1
+ module KOSapiClient
2
+ VERSION = '0.5.0'
3
+ 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