Skip to content
Closed
41 changes: 39 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ A simple Ruby client library for [KOSapi RESTful service](https://kosapi.fit.cvu

Add this line to your application's Gemfile:

gem 'kosapi_client', github: 'flexik/kosapi_client'
gem 'kosapi_client', github: 'flexik/kosapi_client.rb'

And then execute:

Expand Down Expand Up @@ -46,7 +46,7 @@ KOSapiClient can be created and configured in two ways.
The simple way is to call `KOSapiClient.new`, which returns ApiClient instance.

```ruby
client = KOSapiClient.new(OAUTH_CLIENT_ID, OAUTH_SECRET)
client = KOSapiClient.new({client_id: OAUTH_CLIENT_ID, client_secret: OAUTH_SECRET})
client.parallels.find(42)
```

Expand All @@ -63,6 +63,43 @@ end
KOSapiClient.parallels.find(42)
```

## How to extend API functionality

### Manualy explore KOSapi

```ruby
KOSapiClient.configure do |c|
c.client_id = ENV['KOSAPI_OAUTH_CLIENT_ID']
c.client_secret = ENV['KOSAPI_OAUTH_CLIENT_SECRET']
end

puts KOSapiClient.http_client.send_debug_request(:get, '/courses/MI-PAA/instances/B141').to_yaml
```

### Add entity

### Add resource

1. Add concrete resource builder to `lib/kosapi_client/resource/`. Use current resources as an inspiration.
2. Register resource in `lib/kosapi_client/api_client.rb` like:

```ruby
module KOSapiClient

class ApiClient
include ResourceMapper

# accessible resources definition
resource :courses
resource :course_events
resource :parallels
resource :exams
resource :semesters
resource :new_resource

attr_reader :http_client
...
```

## Contributing

Expand Down
3 changes: 1 addition & 2 deletions kosapi_client.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,13 @@ Gem::Specification.new do |spec|
spec.add_development_dependency 'rspec'
spec.add_development_dependency 'rspec-given'
spec.add_development_dependency 'dotenv'
spec.add_development_dependency 'vcr'
spec.add_development_dependency 'codeclimate-test-reporter'
spec.add_development_dependency 'guard-rspec'
spec.add_development_dependency 'coveralls'
spec.add_development_dependency 'fuubar', '~> 2.0.0.rc1'

spec.add_runtime_dependency 'oauth2'
spec.add_runtime_dependency 'faraday', '~> 0.8.9' # VCR does not work with newer versions yet
spec.add_runtime_dependency 'faraday', '~> 0.9.0'
spec.add_runtime_dependency 'activesupport'
spec.add_runtime_dependency 'escape_utils' unless RUBY_PLATFORM == 'java' # used for uri_template
spec.add_runtime_dependency 'uri_template'
Expand Down
3 changes: 2 additions & 1 deletion lib/kosapi_client/api_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ class ApiClient
resource :course_events
resource :parallels
resource :exams

resource :semesters

attr_reader :http_client

##
Expand Down
3 changes: 3 additions & 0 deletions lib/kosapi_client/entity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
require 'kosapi_client/entity/data_mappings'
require 'kosapi_client/entity/base_entity'
require 'kosapi_client/entity/result_page'
require 'kosapi_client/entity/semester'
require 'kosapi_client/entity/coursin'
require 'kosapi_client/entity/course_event'
require 'kosapi_client/entity/course'
require 'kosapi_client/entity/timetable_slot'
Expand All @@ -16,3 +18,4 @@
require 'kosapi_client/entity/teacher'
require 'kosapi_client/entity/student'
require 'kosapi_client/entity/exam'

2 changes: 1 addition & 1 deletion lib/kosapi_client/entity/author.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ def initialize(name)
@name = name
end

def self.parse(contents)
def self.parse(contents, context = {})
new(contents[:atom_name])
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/kosapi_client/entity/boolean.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module KOSapiClient
module Entity
class Boolean

def self.parse(str)
def self.parse(str, context = {})
return true if str == 'true'
return false if str == 'false'
raise "Boolean parsing failed, invalid string: #{str}"
Expand Down
3 changes: 1 addition & 2 deletions lib/kosapi_client/entity/course.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@ class Course < BaseEntity
map_data :superior_course, Link
map_data :subcourses, Link
map_data :tutorials_contents, MLString
map_data :instance #todo

map_data :instance, Coursin
end
end
end
17 changes: 17 additions & 0 deletions lib/kosapi_client/entity/coursin.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module KOSapiClient
module Entity
class Coursin < BaseEntity

map_data :capacity
map_data :capacity_overfill, Integer
map_data :course # TODO: fix circular reference. map_data :course, Course
map_data :occupied, Integer
map_data :semester
map_data :tutorial_capacity, Integer
map_data :examiners, [Link], array_wrapper_element: :teacher
map_data :guarantors, [Link], array_wrapper_element: :teacher
map_data :instructors, [Link], array_wrapper_element: :teacher
map_data :lecturers, [Link], array_wrapper_element: :teacher
end
end
end
37 changes: 23 additions & 14 deletions lib/kosapi_client/entity/data_mappings.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ def to_hash
result
end

def dump
self.to_hash.to_yaml
end

private
def convert_value(val)
if val.respond_to? :to_hash
Expand Down Expand Up @@ -44,34 +48,39 @@ def attr_mappings
# @param [Hash] content hash structure from API response corresponding to single domain object
# @return [BaseEntity] parsed domain object
def parse(content, context = {})
instance = new()
set_mapped_attributes(instance, content)
instance = new
set_mapped_attributes(instance, content, context)
instance
end

# Creates new domain object instance and sets values
# of mapped domain object attributes from source hash.
# Attributes are mapped by .map_data method.
def set_mapped_attributes(instance, source_hash)
def set_mapped_attributes(instance, source_hash, context)
if self.superclass.respond_to? :set_mapped_attributes
self.superclass.set_mapped_attributes(instance, source_hash)
self.superclass.set_mapped_attributes(instance, source_hash, context)
end
raise "Missing data mappings for entity #{self}" unless @data_mappings
@data_mappings.each do |name, options|
set_mapped_attribute(instance, name, source_hash, options)
set_mapped_attribute(instance, name, source_hash, options, context)
end
end

private
def set_mapped_attribute(instance, name, source_hash, mapping_options)
def set_mapped_attribute(instance, name, source_hash, mapping_options, context)
namespace = mapping_options[:namespace]
src_element = mapping_options[:element] || name

if namespace
key = "#{namespace}_#{src_element}".to_sym
else
key = src_element
end

value = source_hash[key]

value = value[mapping_options[:array_wrapper_element]] if mapping_options.key? :array_wrapper_element

if value.nil?
raise "Missing value for attribute #{name}" if mapping_options[:required]
if mapping_options[:type].is_a?(Array)
Expand All @@ -80,32 +89,32 @@ def set_mapped_attribute(instance, name, source_hash, mapping_options)
return
end
else
value = convert_type(value, mapping_options[:type])
value = convert_type(value, mapping_options[:type], context)
end
instance.send("#{name}=".to_sym, value)
end

def convert_type(value, type)
def convert_type(value, type, context = {})
return value.to_i if type == Integer
return value if type == String
return convert_array(value, type.first) if type.is_a?(Array)
return convert_array(value, type.first, context) if type.is_a?(Array)

return type.parse value, context if type.respond_to? :parse

return type.parse(value) if type.respond_to? :parse
raise "Unknown type #{type} to convert value #{value} to."
end

# Converts values of array type to proper domain objects.
# It checks whether the value is really an array, because
# when API returns a single value it does not get parsed
# into an array.
def convert_array(values, type)
def convert_array(values, type, context)
if values.is_a?(Array)
values.map { |it| convert_type(it, type) }
values.map { |it| convert_type(it, type, context) }
else
[ convert_type(values, type) ]
[ convert_type(values, type, context) ]
end
end

end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/kosapi_client/entity/enum.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module KOSapiClient
module Entity
class Enum

def self.parse(contents)
def self.parse(contents, context = {})
contents.downcase.to_sym
end

Expand Down
2 changes: 1 addition & 1 deletion lib/kosapi_client/entity/id.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module KOSapiClient
module Entity
class Id < String

def self.parse(str)
def self.parse(str, context = {})
id = str.split(':').last
new(id)
end
Expand Down
6 changes: 3 additions & 3 deletions lib/kosapi_client/entity/link.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ class Link

attr_reader :link_title, :link_href, :link_rel

def initialize(title, href, rel, client = nil)
def initialize(title, href, rel, client)
@link_title = title
@link_href = escape_url(href)
@link_rel = rel
@client = client
end

def self.parse(contents)
def self.parse(contents, context)
href = contents[:xlink_href] || contents[:href]
new(contents[:__content__], href, contents[:rel])
new(contents[:__content__], href, contents[:rel], context[:client])
end

def link_id
Expand Down
2 changes: 1 addition & 1 deletion lib/kosapi_client/entity/ml_string.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def to_s(lang = :implicit)
@translations[lang]
end

def self.parse(item)
def self.parse(item, context = {})
unless item.is_a?(Array)
item = [item]
end
Expand Down
11 changes: 11 additions & 0 deletions lib/kosapi_client/entity/semester.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module KOSapiClient
module Entity
class Semester < BaseEntity

map_data :name, MLString
map_data :end_date, Time
map_data :start_date, Time

end
end
end
26 changes: 21 additions & 5 deletions lib/kosapi_client/http_client.rb
Original file line number Diff line number Diff line change
@@ -1,22 +1,32 @@
module KOSapiClient
class HTTPClient

def initialize(http_adapter, preprocessor = ResponsePreprocessor.new, converter = ResponseConverter.new(self))
def initialize(http_adapter, preprocessor = ResponsePreprocessor.new, converter = ResponseConverter.new)
@http_adapter = http_adapter
@preprocessor = preprocessor
@converter = converter
end

def send_request(verb, url, options = {})
def send_debug_request(verb, url, options = {})
send_request verb, url, options, true
end

def send_request(verb, url, options = {}, debug_request = false)
absolute_url = get_absolute_url(url)

p ">> #{absolute_url}" if debug_request

result = @http_adapter.send_request(verb, absolute_url, options)
process_response(result)
process_response(result, debug_request)
end

def process_response(result)
def process_response(result, debug_request = false)
preprocessed = @preprocessor.preprocess(result)

return preprocessed if debug_request

response = KOSapiClient::KOSapiResponse.new(preprocessed)
@converter.convert(response)
@converter.convert(response, create_context)
end

def get_absolute_url(url)
Expand All @@ -27,6 +37,12 @@ def get_absolute_url(url)
end
end

def create_context
{
client: self
}
end

private
def is_absolute(url)
url.start_with?('http')
Expand Down
5 changes: 5 additions & 0 deletions lib/kosapi_client/request_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ def limit(num)
self
end

def sem(code)
@url_builder.set_query_param(:sem, code)
self
end

def query(params = {})
raise 'Empty parameters to query are not allowed' if params.empty?
if params.instance_of?(String)
Expand Down
1 change: 1 addition & 0 deletions lib/kosapi_client/resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require 'kosapi_client/resource/parallels_builder'
require 'kosapi_client/resource/exams_builder'
require 'kosapi_client/resource/course_events_builder'
require 'kosapi_client/resource/semesters_builder'

module KOSapiClient
module Resource
Expand Down
Loading