From d647d3cbdf14496a55dc4c8bfdd15eba198836e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pol=C3=ADvka?= Date: Thu, 5 Mar 2015 16:32:01 +0100 Subject: [PATCH 1/6] Pass context through parsing process to enable following all content API references --- lib/kosapi_client/entity/author.rb | 2 +- lib/kosapi_client/entity/boolean.rb | 2 +- lib/kosapi_client/entity/data_mappings.rb | 30 +++++++++++-------- lib/kosapi_client/entity/enum.rb | 2 +- lib/kosapi_client/entity/id.rb | 2 +- lib/kosapi_client/entity/link.rb | 12 ++++---- lib/kosapi_client/entity/ml_string.rb | 2 +- lib/kosapi_client/http_client.rb | 12 ++++++-- lib/kosapi_client/kosapi_client.rb | 5 +++- lib/kosapi_client/response_converter.rb | 30 ++++++++----------- lib/kosapi_client/response_links.rb | 13 ++++---- spec/integration/parallels_spec.rb | 6 ++++ spec/kosapi_client/entity/link_spec.rb | 10 ++----- spec/kosapi_client/entity/parallel_spec.rb | 14 ++++++--- spec/kosapi_client/entity/result_page_spec.rb | 12 ++++++-- spec/kosapi_client/response_converter_spec.rb | 18 ++++++----- spec/kosapi_client/response_links_spec.rb | 13 ++++---- 17 files changed, 102 insertions(+), 83 deletions(-) diff --git a/lib/kosapi_client/entity/author.rb b/lib/kosapi_client/entity/author.rb index d5f9ef6..b3042d4 100644 --- a/lib/kosapi_client/entity/author.rb +++ b/lib/kosapi_client/entity/author.rb @@ -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 diff --git a/lib/kosapi_client/entity/boolean.rb b/lib/kosapi_client/entity/boolean.rb index 741a6fd..aa417cc 100644 --- a/lib/kosapi_client/entity/boolean.rb +++ b/lib/kosapi_client/entity/boolean.rb @@ -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}" diff --git a/lib/kosapi_client/entity/data_mappings.rb b/lib/kosapi_client/entity/data_mappings.rb index e843466..c76a4b2 100644 --- a/lib/kosapi_client/entity/data_mappings.rb +++ b/lib/kosapi_client/entity/data_mappings.rb @@ -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 @@ -44,26 +48,26 @@ 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 @@ -80,17 +84,17 @@ 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) if type.respond_to? :parse + return type.parse(value, context) if type.respond_to? :parse raise "Unknown type #{type} to convert value #{value} to." end @@ -98,11 +102,11 @@ def convert_type(value, type) # 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 diff --git a/lib/kosapi_client/entity/enum.rb b/lib/kosapi_client/entity/enum.rb index 09aa3b4..4a80b35 100644 --- a/lib/kosapi_client/entity/enum.rb +++ b/lib/kosapi_client/entity/enum.rb @@ -2,7 +2,7 @@ module KOSapiClient module Entity class Enum - def self.parse(contents) + def self.parse(contents, context = {}) contents.downcase.to_sym end diff --git a/lib/kosapi_client/entity/id.rb b/lib/kosapi_client/entity/id.rb index 54fcb27..3113007 100644 --- a/lib/kosapi_client/entity/id.rb +++ b/lib/kosapi_client/entity/id.rb @@ -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 diff --git a/lib/kosapi_client/entity/link.rb b/lib/kosapi_client/entity/link.rb index 1439e92..bcd7e79 100644 --- a/lib/kosapi_client/entity/link.rb +++ b/lib/kosapi_client/entity/link.rb @@ -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 @@ -21,14 +21,12 @@ def link_id end def follow + return @target unless @target.nil? + raise "HTTP client not set, cannot send request to #{link_href}" unless @client @client.send_request(:get, link_href) end - def inject_client(client) - @client = client - end - def target @target ||= follow end diff --git a/lib/kosapi_client/entity/ml_string.rb b/lib/kosapi_client/entity/ml_string.rb index 3a9fd6a..1f97fcc 100644 --- a/lib/kosapi_client/entity/ml_string.rb +++ b/lib/kosapi_client/entity/ml_string.rb @@ -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 diff --git a/lib/kosapi_client/http_client.rb b/lib/kosapi_client/http_client.rb index 4885971..17bff6f 100644 --- a/lib/kosapi_client/http_client.rb +++ b/lib/kosapi_client/http_client.rb @@ -1,7 +1,7 @@ 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 @@ -15,8 +15,8 @@ def send_request(verb, url, options = {}) def process_response(result) preprocessed = @preprocessor.preprocess(result) - response = KOSapiClient::KOSapiResponse.new(preprocessed) - @converter.convert(response) + response = KOSapiClient::KOSapiResponse.new preprocessed + @converter.convert response, create_context end def get_absolute_url(url) @@ -32,5 +32,11 @@ def is_absolute(url) url.start_with?('http') end + def create_context + { + client: self + } + end + end end diff --git a/lib/kosapi_client/kosapi_client.rb b/lib/kosapi_client/kosapi_client.rb index 0d86ac3..c9616e4 100644 --- a/lib/kosapi_client/kosapi_client.rb +++ b/lib/kosapi_client/kosapi_client.rb @@ -39,6 +39,9 @@ def respond_to_missing?(method_name, include_private = false) @client.respond_to?(method_name, include_private) end + # Was interfering with mocking + def to_str + "KOSapi client" + end end - end diff --git a/lib/kosapi_client/response_converter.rb b/lib/kosapi_client/response_converter.rb index 6aee7fe..8cf323a 100644 --- a/lib/kosapi_client/response_converter.rb +++ b/lib/kosapi_client/response_converter.rb @@ -6,16 +6,11 @@ module KOSapiClient # determined at runtime based on API response. class ResponseConverter - - def initialize(client) - @client = client - end - - def convert(response) + def convert(response, context = {}) if response.is_paginated? - convert_paginated(response) + convert_paginated(response, context) else - convert_single(response.item) + convert_single(response.item, context) end end @@ -24,20 +19,20 @@ def convert(response) # @param response [KOSapiResponse] Response object wrapping array of hashes corresponding to entries # @return [ResultPage] ResultPage of domain objects - def convert_paginated(response) + def convert_paginated(response, context) items = response.items || [] - converted_items = items.map{ |p| convert_single(p) } - Entity::ResultPage.new(converted_items, create_links(response)) + converted_items = items.map{ |p| convert_single(p, context) } + Entity::ResultPage.new(converted_items, create_links(response, context)) end - def convert_single(item) + def convert_single(item, context) type = detect_type(item) - convert_type(item, type) + convert_type(item, type, context) end private - def convert_type(hash, type) - type.parse(hash) + def convert_type(hash, type, context) + type.parse(hash, context) end def detect_type(hash) @@ -55,9 +50,8 @@ def extract_type(type_str) entity_type end - def create_links(response) - ResponseLinks.parse(response.links_hash, @client) + def create_links(response, context) + ResponseLinks.parse(response.links_hash, context) end - end end diff --git a/lib/kosapi_client/response_links.rb b/lib/kosapi_client/response_links.rb index dc7a2c1..eb699d2 100644 --- a/lib/kosapi_client/response_links.rb +++ b/lib/kosapi_client/response_links.rb @@ -12,20 +12,19 @@ def initialize(prev_link, next_link) class << self - def parse(hash, client) - prev_link = parse_link(hash, 'prev', client) - next_link = parse_link(hash, 'next', client) + def parse(hash, context) + prev_link = parse_link(hash, 'prev', context) + next_link = parse_link(hash, 'next', context) new(prev_link, next_link) end private - def parse_link(hash, rel, client) + def parse_link(hash, rel, context) return nil unless hash link_hash = extract_link_hash(hash, rel) + if link_hash - link = Entity::Link.parse(link_hash) - link.inject_client(client) - link + Entity::Link.parse(link_hash, context) end end diff --git a/spec/integration/parallels_spec.rb b/spec/integration/parallels_spec.rb index 35117e4..848d3c2 100644 --- a/spec/integration/parallels_spec.rb +++ b/spec/integration/parallels_spec.rb @@ -43,6 +43,12 @@ expect(parallel.link.link_rel).not_to be_nil end + it 'follows reference link properly' do + teacher = client.parallels.find(339540000).teachers.first + + expect(teacher.username).to eq("balikm") + end + it 'parses timetable slot ID' do page = client.parallels slot = page.items.first.timetable_slots.first diff --git a/spec/kosapi_client/entity/link_spec.rb b/spec/kosapi_client/entity/link_spec.rb index ab233c3..39ea939 100644 --- a/spec/kosapi_client/entity/link_spec.rb +++ b/spec/kosapi_client/entity/link_spec.rb @@ -5,9 +5,8 @@ Link = KOSapiClient::Entity::Link let(:client) { instance_double(KOSapiClient::HTTPClient) } - subject(:link) { Link.parse({href: 'http://example.com/foo/bar/42', __content__: 'Example Site', rel: 'next'}) } + subject(:link) { Link.parse({href: 'http://example.com/foo/bar/42', __content__: 'Example Site', rel: 'next'}, {client: client}) } let(:result) { double(:result, foo: :bar) } - before(:example) { link.inject_client(client) } describe '.parse' do @@ -23,7 +22,7 @@ it 'encodes href URL' do href = 'parallels?query=(lastUpdatedDate%3E=2014-07-01T00:00:00;lastUpdatedDate%3C=2014-07-10T00:00:00)&offset=10&limit=10' - link = Link.new(nil, href, nil) + link = Link.new(nil, href, nil, nil) expect(link.link_href).to eq 'parallels?query=(lastUpdatedDate%3E=2014-07-01T00:00:00%3BlastUpdatedDate%3C=2014-07-10T00:00:00)&offset=10&limit=10' end @@ -39,11 +38,6 @@ describe '#follow' do - it 'throws error when not http client set' do - link.inject_client(nil) - expect { link.follow }.to raise_error(RuntimeError) - end - it 'calls http client with href' do expect(client).to receive(:send_request).with(:get, 'http://example.com/foo/bar/42') link.follow diff --git a/spec/kosapi_client/entity/parallel_spec.rb b/spec/kosapi_client/entity/parallel_spec.rb index 7246e7f..51816e8 100644 --- a/spec/kosapi_client/entity/parallel_spec.rb +++ b/spec/kosapi_client/entity/parallel_spec.rb @@ -6,13 +6,19 @@ teacher: [{ xlink_href: 'teachers/smitkdan/', __content__: 'Ing. arch. Daniel Smitka Ph.D.' }] } } + let(:client) { instance_double(KOSapiClient::HTTPClient) } it 'parses parallel attributes' do - parallel = KOSapiClient::Entity::Parallel.parse(attributes) + parallel = KOSapiClient::Entity::Parallel.parse(attributes, {client: client}) expect(parallel.code).to eq 42 expect(parallel.capacity_overfill).to eq :denied - expect(parallel.teachers.first).to be_an_instance_of KOSapiClient::Entity::Link - expect(parallel.teachers.first.link_href).to eq 'teachers/smitkdan/' - expect(parallel.teachers.first.link_title).to eq 'Ing. arch. Daniel Smitka Ph.D.' + expect(parallel.teachers).to be_a(Array) + expect(parallel.teachers.first).to be_instance_of KOSapiClient::Entity::Link + + link_data = parallel.teachers.first.to_hash + + expect(link_data[:href]).to eq 'teachers/smitkdan/' + expect(link_data[:title]).to eq 'Ing. arch. Daniel Smitka Ph.D.' + end end diff --git a/spec/kosapi_client/entity/result_page_spec.rb b/spec/kosapi_client/entity/result_page_spec.rb index 25edd61..5a997d0 100644 --- a/spec/kosapi_client/entity/result_page_spec.rb +++ b/spec/kosapi_client/entity/result_page_spec.rb @@ -4,16 +4,22 @@ ResultPage = KOSapiClient::Entity::ResultPage + let(:links) { KOSapiClient::ResponseLinks.new(nil, next_link) } subject(:result_page) { ResultPage.new([item], links) } let(:item) { double(:item) } let(:item2) { double(:second_item) } - let(:links) { instance_double(KOSapiClient::ResponseLinks, next: next_link) } - let(:next_page) { ResultPage.new([item2], instance_double(KOSapiClient::ResponseLinks, next: nil)) } - let(:next_link) { instance_double(KOSapiClient::Entity::Link, follow: next_page) } + let(:next_page) { ResultPage.new([item2], KOSapiClient::ResponseLinks.new(nil, nil)) } + let(:next_link) { link = KOSapiClient::Entity::Link.new(nil, "/", nil, nil) } + + before(:each) do + next_link.instance_variable_set(:@target, next_page) + end + describe '#each' do it 'is auto-paginated by default' do + [item, item2].each { |it| expect(it).to receive(:foo) } result_page.each { |it| it.foo } end diff --git a/spec/kosapi_client/response_converter_spec.rb b/spec/kosapi_client/response_converter_spec.rb index 4280b80..3aebc32 100644 --- a/spec/kosapi_client/response_converter_spec.rb +++ b/spec/kosapi_client/response_converter_spec.rb @@ -3,31 +3,33 @@ describe KOSapiClient::ResponseConverter do let(:client) { instance_double(KOSapiClient::HTTPClient) } - subject(:converter) { described_class.new(client) } + subject(:converter) { described_class.new } + let(:converter_context) { {client: client} } describe '#convert' do context 'with paginated response' do - let(:next_link) { instance_double(KOSapiClient::Entity::Link) } let(:prev_link) { instance_double(KOSapiClient::Entity::Link) } - let(:links) { instance_double(KOSapiClient::ResponseLinks, next: next_link, prev: prev_link) } + let(:next_link) { instance_double(KOSapiClient::Entity::Link) } + let(:links) { KOSapiClient::ResponseLinks.new(prev_link, next_link) } + let(:api_response) { double(is_paginated?: true, items: [{xsi_type: 'courseEvent', capacity: 70}, {xsi_type: 'courseEvent', capacity: 40}], links_hash: links) } before(:each) { allow(converter).to receive(:create_links).and_return(links) } it 'processes paginated response' do - result = converter.convert(api_response) + result = converter.convert(api_response, converter_context) expect(result).to be_an_instance_of(KOSapiClient::Entity::ResultPage) end it 'creates next link' do - result = converter.convert(api_response) + result = converter.convert(api_response, converter_context) expect(result.next).to be next_link end it 'creates prev link' do - result = converter.convert(api_response) + result = converter.convert(api_response, converter_context) expect(result.prev).to be prev_link end end @@ -37,7 +39,7 @@ let(:api_response) { double(is_paginated?: false, item: {xsi_type: 'courseEvent', capacity: 70}) } it 'processes non-paginated response' do - result = converter.convert(api_response) + result = converter.convert(api_response, converter_context) expect(result).to be_an_instance_of(KOSapiClient::Entity::CourseEvent) end @@ -48,7 +50,7 @@ let(:api_response) { double(is_paginated?: false, item: {xsi_type: 'unknownType'}) } it 'raises error when type not found' do - expect { converter.convert(api_response) }.to raise_error(RuntimeError) + expect { converter.convert(api_response, converter_context) }.to raise_error(RuntimeError) end end diff --git a/spec/kosapi_client/response_links_spec.rb b/spec/kosapi_client/response_links_spec.rb index 4356bf8..d356ddc 100644 --- a/spec/kosapi_client/response_links_spec.rb +++ b/spec/kosapi_client/response_links_spec.rb @@ -2,14 +2,15 @@ describe KOSapiClient::ResponseLinks do - let(:client) { instance_double(KOSapiClient::HTTPClient) } subject(:links) { described_class.new } + let(:client) { instance_double(KOSapiClient::HTTPClient) } + let(:parse_context) { {client: client}} describe '.parse' do context 'with no links' do it 'returns instance with no links set' do - links = described_class.parse(nil, client) + links = described_class.parse(nil, parse_context) expect(links.next).not_to be expect(links.prev).not_to be end @@ -17,7 +18,7 @@ context 'with both links' do it 'parses both links' do - links = described_class.parse([{rel: 'prev', href: 'courses/?offset=0&limit=10'}, {rel: 'next', href: 'courses/?offset=20&limit=10'}], client) + links = described_class.parse([{rel: 'prev', href: 'courses/?offset=0&limit=10'}, {rel: 'next', href: 'courses/?offset=20&limit=10'}], parse_context) expect(links.next.link_href).to eq 'courses/?offset=20&limit=10' expect(links.prev.link_href).to eq 'courses/?offset=0&limit=10' end @@ -25,7 +26,7 @@ context 'with next link' do it 'parses next link' do - links = described_class.parse({rel: 'next', href: 'courses/?offset=20&limit=10'}, client) + links = described_class.parse({rel: 'next', href: 'courses/?offset=20&limit=10'}, parse_context) expect(links.next.link_href).to eq 'courses/?offset=20&limit=10' expect(links.next.link_rel).to eq 'next' expect(links.prev).not_to be @@ -34,7 +35,7 @@ context 'with prev link' do it 'parses prev link' do - links = described_class.parse({rel: 'prev', href: 'courses/?offset=20&limit=10'}, client) + links = described_class.parse({rel: 'prev', href: 'courses/?offset=20&limit=10'}, parse_context) expect(links.next).not_to be expect(links.prev.link_href).to eq 'courses/?offset=20&limit=10' expect(links.prev.link_rel).to eq 'prev' @@ -42,7 +43,7 @@ end it 'injects http client' do - links = described_class.parse({rel: 'next', href: 'courses/?offset=20&limit=10'}, client) + links = described_class.parse({rel: 'next', href: 'courses/?offset=20&limit=10'}, parse_context) expect(client).to receive(:send_request) links.next.follow end From 9cb112fa31319b4e56593497cc791f2ba05aa86e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pol=C3=ADvka?= Date: Sun, 8 Mar 2015 23:53:58 +0100 Subject: [PATCH 2/6] Mocking fix refactored Less hackier way to fix mocking invoking `to_str` method which resulted to raising error with missing client even from separeted units of API client (as signal was bubbling to top module) --- lib/kosapi_client/kosapi_client.rb | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/kosapi_client/kosapi_client.rb b/lib/kosapi_client/kosapi_client.rb index c9616e4..9ac28b8 100644 --- a/lib/kosapi_client/kosapi_client.rb +++ b/lib/kosapi_client/kosapi_client.rb @@ -5,6 +5,8 @@ module KOSapiClient singleton_class.class_eval do attr_reader :client + + alias_method :to_str, :to_s def new(credentials, base_url = DEFAULT_KOSAPI_BASE_URL) http_adapter = OAuth2HttpAdapter.new(credentials, base_url) @@ -38,10 +40,5 @@ def method_missing(method, *args, &block) def respond_to_missing?(method_name, include_private = false) @client.respond_to?(method_name, include_private) end - - # Was interfering with mocking - def to_str - "KOSapi client" - end end end From 2e79897506b1c8f3ef2146e19026d722c5852244 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pol=C3=ADvka?= Date: Thu, 5 Mar 2015 16:32:01 +0100 Subject: [PATCH 3/6] Pass context through parsing process to enable following all content API references --- lib/kosapi_client/entity/author.rb | 2 +- lib/kosapi_client/entity/boolean.rb | 2 +- lib/kosapi_client/entity/data_mappings.rb | 30 +++++++++++-------- lib/kosapi_client/entity/enum.rb | 2 +- lib/kosapi_client/entity/id.rb | 2 +- lib/kosapi_client/entity/link.rb | 12 ++++---- lib/kosapi_client/entity/ml_string.rb | 2 +- lib/kosapi_client/http_client.rb | 12 ++++++-- lib/kosapi_client/kosapi_client.rb | 5 +++- lib/kosapi_client/response_converter.rb | 30 ++++++++----------- lib/kosapi_client/response_links.rb | 13 ++++---- spec/integration/parallels_spec.rb | 6 ++++ spec/kosapi_client/entity/link_spec.rb | 10 ++----- spec/kosapi_client/entity/parallel_spec.rb | 14 ++++++--- spec/kosapi_client/entity/result_page_spec.rb | 12 ++++++-- spec/kosapi_client/response_converter_spec.rb | 18 ++++++----- spec/kosapi_client/response_links_spec.rb | 13 ++++---- 17 files changed, 102 insertions(+), 83 deletions(-) diff --git a/lib/kosapi_client/entity/author.rb b/lib/kosapi_client/entity/author.rb index d5f9ef6..b3042d4 100644 --- a/lib/kosapi_client/entity/author.rb +++ b/lib/kosapi_client/entity/author.rb @@ -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 diff --git a/lib/kosapi_client/entity/boolean.rb b/lib/kosapi_client/entity/boolean.rb index 741a6fd..aa417cc 100644 --- a/lib/kosapi_client/entity/boolean.rb +++ b/lib/kosapi_client/entity/boolean.rb @@ -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}" diff --git a/lib/kosapi_client/entity/data_mappings.rb b/lib/kosapi_client/entity/data_mappings.rb index e843466..c76a4b2 100644 --- a/lib/kosapi_client/entity/data_mappings.rb +++ b/lib/kosapi_client/entity/data_mappings.rb @@ -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 @@ -44,26 +48,26 @@ 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 @@ -80,17 +84,17 @@ 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) if type.respond_to? :parse + return type.parse(value, context) if type.respond_to? :parse raise "Unknown type #{type} to convert value #{value} to." end @@ -98,11 +102,11 @@ def convert_type(value, type) # 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 diff --git a/lib/kosapi_client/entity/enum.rb b/lib/kosapi_client/entity/enum.rb index 09aa3b4..4a80b35 100644 --- a/lib/kosapi_client/entity/enum.rb +++ b/lib/kosapi_client/entity/enum.rb @@ -2,7 +2,7 @@ module KOSapiClient module Entity class Enum - def self.parse(contents) + def self.parse(contents, context = {}) contents.downcase.to_sym end diff --git a/lib/kosapi_client/entity/id.rb b/lib/kosapi_client/entity/id.rb index 54fcb27..3113007 100644 --- a/lib/kosapi_client/entity/id.rb +++ b/lib/kosapi_client/entity/id.rb @@ -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 diff --git a/lib/kosapi_client/entity/link.rb b/lib/kosapi_client/entity/link.rb index 1439e92..bcd7e79 100644 --- a/lib/kosapi_client/entity/link.rb +++ b/lib/kosapi_client/entity/link.rb @@ -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 @@ -21,14 +21,12 @@ def link_id end def follow + return @target unless @target.nil? + raise "HTTP client not set, cannot send request to #{link_href}" unless @client @client.send_request(:get, link_href) end - def inject_client(client) - @client = client - end - def target @target ||= follow end diff --git a/lib/kosapi_client/entity/ml_string.rb b/lib/kosapi_client/entity/ml_string.rb index 3a9fd6a..1f97fcc 100644 --- a/lib/kosapi_client/entity/ml_string.rb +++ b/lib/kosapi_client/entity/ml_string.rb @@ -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 diff --git a/lib/kosapi_client/http_client.rb b/lib/kosapi_client/http_client.rb index 4885971..17bff6f 100644 --- a/lib/kosapi_client/http_client.rb +++ b/lib/kosapi_client/http_client.rb @@ -1,7 +1,7 @@ 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 @@ -15,8 +15,8 @@ def send_request(verb, url, options = {}) def process_response(result) preprocessed = @preprocessor.preprocess(result) - response = KOSapiClient::KOSapiResponse.new(preprocessed) - @converter.convert(response) + response = KOSapiClient::KOSapiResponse.new preprocessed + @converter.convert response, create_context end def get_absolute_url(url) @@ -32,5 +32,11 @@ def is_absolute(url) url.start_with?('http') end + def create_context + { + client: self + } + end + end end diff --git a/lib/kosapi_client/kosapi_client.rb b/lib/kosapi_client/kosapi_client.rb index 8b1dc13..086ba19 100644 --- a/lib/kosapi_client/kosapi_client.rb +++ b/lib/kosapi_client/kosapi_client.rb @@ -40,6 +40,9 @@ def config @config ||= Configuration.new end + # Was interfering with mocking + def to_str + "KOSapi client" + end end - end diff --git a/lib/kosapi_client/response_converter.rb b/lib/kosapi_client/response_converter.rb index 6aee7fe..8cf323a 100644 --- a/lib/kosapi_client/response_converter.rb +++ b/lib/kosapi_client/response_converter.rb @@ -6,16 +6,11 @@ module KOSapiClient # determined at runtime based on API response. class ResponseConverter - - def initialize(client) - @client = client - end - - def convert(response) + def convert(response, context = {}) if response.is_paginated? - convert_paginated(response) + convert_paginated(response, context) else - convert_single(response.item) + convert_single(response.item, context) end end @@ -24,20 +19,20 @@ def convert(response) # @param response [KOSapiResponse] Response object wrapping array of hashes corresponding to entries # @return [ResultPage] ResultPage of domain objects - def convert_paginated(response) + def convert_paginated(response, context) items = response.items || [] - converted_items = items.map{ |p| convert_single(p) } - Entity::ResultPage.new(converted_items, create_links(response)) + converted_items = items.map{ |p| convert_single(p, context) } + Entity::ResultPage.new(converted_items, create_links(response, context)) end - def convert_single(item) + def convert_single(item, context) type = detect_type(item) - convert_type(item, type) + convert_type(item, type, context) end private - def convert_type(hash, type) - type.parse(hash) + def convert_type(hash, type, context) + type.parse(hash, context) end def detect_type(hash) @@ -55,9 +50,8 @@ def extract_type(type_str) entity_type end - def create_links(response) - ResponseLinks.parse(response.links_hash, @client) + def create_links(response, context) + ResponseLinks.parse(response.links_hash, context) end - end end diff --git a/lib/kosapi_client/response_links.rb b/lib/kosapi_client/response_links.rb index dc7a2c1..eb699d2 100644 --- a/lib/kosapi_client/response_links.rb +++ b/lib/kosapi_client/response_links.rb @@ -12,20 +12,19 @@ def initialize(prev_link, next_link) class << self - def parse(hash, client) - prev_link = parse_link(hash, 'prev', client) - next_link = parse_link(hash, 'next', client) + def parse(hash, context) + prev_link = parse_link(hash, 'prev', context) + next_link = parse_link(hash, 'next', context) new(prev_link, next_link) end private - def parse_link(hash, rel, client) + def parse_link(hash, rel, context) return nil unless hash link_hash = extract_link_hash(hash, rel) + if link_hash - link = Entity::Link.parse(link_hash) - link.inject_client(client) - link + Entity::Link.parse(link_hash, context) end end diff --git a/spec/integration/parallels_spec.rb b/spec/integration/parallels_spec.rb index 35117e4..848d3c2 100644 --- a/spec/integration/parallels_spec.rb +++ b/spec/integration/parallels_spec.rb @@ -43,6 +43,12 @@ expect(parallel.link.link_rel).not_to be_nil end + it 'follows reference link properly' do + teacher = client.parallels.find(339540000).teachers.first + + expect(teacher.username).to eq("balikm") + end + it 'parses timetable slot ID' do page = client.parallels slot = page.items.first.timetable_slots.first diff --git a/spec/kosapi_client/entity/link_spec.rb b/spec/kosapi_client/entity/link_spec.rb index ab233c3..39ea939 100644 --- a/spec/kosapi_client/entity/link_spec.rb +++ b/spec/kosapi_client/entity/link_spec.rb @@ -5,9 +5,8 @@ Link = KOSapiClient::Entity::Link let(:client) { instance_double(KOSapiClient::HTTPClient) } - subject(:link) { Link.parse({href: 'http://example.com/foo/bar/42', __content__: 'Example Site', rel: 'next'}) } + subject(:link) { Link.parse({href: 'http://example.com/foo/bar/42', __content__: 'Example Site', rel: 'next'}, {client: client}) } let(:result) { double(:result, foo: :bar) } - before(:example) { link.inject_client(client) } describe '.parse' do @@ -23,7 +22,7 @@ it 'encodes href URL' do href = 'parallels?query=(lastUpdatedDate%3E=2014-07-01T00:00:00;lastUpdatedDate%3C=2014-07-10T00:00:00)&offset=10&limit=10' - link = Link.new(nil, href, nil) + link = Link.new(nil, href, nil, nil) expect(link.link_href).to eq 'parallels?query=(lastUpdatedDate%3E=2014-07-01T00:00:00%3BlastUpdatedDate%3C=2014-07-10T00:00:00)&offset=10&limit=10' end @@ -39,11 +38,6 @@ describe '#follow' do - it 'throws error when not http client set' do - link.inject_client(nil) - expect { link.follow }.to raise_error(RuntimeError) - end - it 'calls http client with href' do expect(client).to receive(:send_request).with(:get, 'http://example.com/foo/bar/42') link.follow diff --git a/spec/kosapi_client/entity/parallel_spec.rb b/spec/kosapi_client/entity/parallel_spec.rb index 7246e7f..51816e8 100644 --- a/spec/kosapi_client/entity/parallel_spec.rb +++ b/spec/kosapi_client/entity/parallel_spec.rb @@ -6,13 +6,19 @@ teacher: [{ xlink_href: 'teachers/smitkdan/', __content__: 'Ing. arch. Daniel Smitka Ph.D.' }] } } + let(:client) { instance_double(KOSapiClient::HTTPClient) } it 'parses parallel attributes' do - parallel = KOSapiClient::Entity::Parallel.parse(attributes) + parallel = KOSapiClient::Entity::Parallel.parse(attributes, {client: client}) expect(parallel.code).to eq 42 expect(parallel.capacity_overfill).to eq :denied - expect(parallel.teachers.first).to be_an_instance_of KOSapiClient::Entity::Link - expect(parallel.teachers.first.link_href).to eq 'teachers/smitkdan/' - expect(parallel.teachers.first.link_title).to eq 'Ing. arch. Daniel Smitka Ph.D.' + expect(parallel.teachers).to be_a(Array) + expect(parallel.teachers.first).to be_instance_of KOSapiClient::Entity::Link + + link_data = parallel.teachers.first.to_hash + + expect(link_data[:href]).to eq 'teachers/smitkdan/' + expect(link_data[:title]).to eq 'Ing. arch. Daniel Smitka Ph.D.' + end end diff --git a/spec/kosapi_client/entity/result_page_spec.rb b/spec/kosapi_client/entity/result_page_spec.rb index 25edd61..5a997d0 100644 --- a/spec/kosapi_client/entity/result_page_spec.rb +++ b/spec/kosapi_client/entity/result_page_spec.rb @@ -4,16 +4,22 @@ ResultPage = KOSapiClient::Entity::ResultPage + let(:links) { KOSapiClient::ResponseLinks.new(nil, next_link) } subject(:result_page) { ResultPage.new([item], links) } let(:item) { double(:item) } let(:item2) { double(:second_item) } - let(:links) { instance_double(KOSapiClient::ResponseLinks, next: next_link) } - let(:next_page) { ResultPage.new([item2], instance_double(KOSapiClient::ResponseLinks, next: nil)) } - let(:next_link) { instance_double(KOSapiClient::Entity::Link, follow: next_page) } + let(:next_page) { ResultPage.new([item2], KOSapiClient::ResponseLinks.new(nil, nil)) } + let(:next_link) { link = KOSapiClient::Entity::Link.new(nil, "/", nil, nil) } + + before(:each) do + next_link.instance_variable_set(:@target, next_page) + end + describe '#each' do it 'is auto-paginated by default' do + [item, item2].each { |it| expect(it).to receive(:foo) } result_page.each { |it| it.foo } end diff --git a/spec/kosapi_client/response_converter_spec.rb b/spec/kosapi_client/response_converter_spec.rb index 4280b80..3aebc32 100644 --- a/spec/kosapi_client/response_converter_spec.rb +++ b/spec/kosapi_client/response_converter_spec.rb @@ -3,31 +3,33 @@ describe KOSapiClient::ResponseConverter do let(:client) { instance_double(KOSapiClient::HTTPClient) } - subject(:converter) { described_class.new(client) } + subject(:converter) { described_class.new } + let(:converter_context) { {client: client} } describe '#convert' do context 'with paginated response' do - let(:next_link) { instance_double(KOSapiClient::Entity::Link) } let(:prev_link) { instance_double(KOSapiClient::Entity::Link) } - let(:links) { instance_double(KOSapiClient::ResponseLinks, next: next_link, prev: prev_link) } + let(:next_link) { instance_double(KOSapiClient::Entity::Link) } + let(:links) { KOSapiClient::ResponseLinks.new(prev_link, next_link) } + let(:api_response) { double(is_paginated?: true, items: [{xsi_type: 'courseEvent', capacity: 70}, {xsi_type: 'courseEvent', capacity: 40}], links_hash: links) } before(:each) { allow(converter).to receive(:create_links).and_return(links) } it 'processes paginated response' do - result = converter.convert(api_response) + result = converter.convert(api_response, converter_context) expect(result).to be_an_instance_of(KOSapiClient::Entity::ResultPage) end it 'creates next link' do - result = converter.convert(api_response) + result = converter.convert(api_response, converter_context) expect(result.next).to be next_link end it 'creates prev link' do - result = converter.convert(api_response) + result = converter.convert(api_response, converter_context) expect(result.prev).to be prev_link end end @@ -37,7 +39,7 @@ let(:api_response) { double(is_paginated?: false, item: {xsi_type: 'courseEvent', capacity: 70}) } it 'processes non-paginated response' do - result = converter.convert(api_response) + result = converter.convert(api_response, converter_context) expect(result).to be_an_instance_of(KOSapiClient::Entity::CourseEvent) end @@ -48,7 +50,7 @@ let(:api_response) { double(is_paginated?: false, item: {xsi_type: 'unknownType'}) } it 'raises error when type not found' do - expect { converter.convert(api_response) }.to raise_error(RuntimeError) + expect { converter.convert(api_response, converter_context) }.to raise_error(RuntimeError) end end diff --git a/spec/kosapi_client/response_links_spec.rb b/spec/kosapi_client/response_links_spec.rb index 4356bf8..d356ddc 100644 --- a/spec/kosapi_client/response_links_spec.rb +++ b/spec/kosapi_client/response_links_spec.rb @@ -2,14 +2,15 @@ describe KOSapiClient::ResponseLinks do - let(:client) { instance_double(KOSapiClient::HTTPClient) } subject(:links) { described_class.new } + let(:client) { instance_double(KOSapiClient::HTTPClient) } + let(:parse_context) { {client: client}} describe '.parse' do context 'with no links' do it 'returns instance with no links set' do - links = described_class.parse(nil, client) + links = described_class.parse(nil, parse_context) expect(links.next).not_to be expect(links.prev).not_to be end @@ -17,7 +18,7 @@ context 'with both links' do it 'parses both links' do - links = described_class.parse([{rel: 'prev', href: 'courses/?offset=0&limit=10'}, {rel: 'next', href: 'courses/?offset=20&limit=10'}], client) + links = described_class.parse([{rel: 'prev', href: 'courses/?offset=0&limit=10'}, {rel: 'next', href: 'courses/?offset=20&limit=10'}], parse_context) expect(links.next.link_href).to eq 'courses/?offset=20&limit=10' expect(links.prev.link_href).to eq 'courses/?offset=0&limit=10' end @@ -25,7 +26,7 @@ context 'with next link' do it 'parses next link' do - links = described_class.parse({rel: 'next', href: 'courses/?offset=20&limit=10'}, client) + links = described_class.parse({rel: 'next', href: 'courses/?offset=20&limit=10'}, parse_context) expect(links.next.link_href).to eq 'courses/?offset=20&limit=10' expect(links.next.link_rel).to eq 'next' expect(links.prev).not_to be @@ -34,7 +35,7 @@ context 'with prev link' do it 'parses prev link' do - links = described_class.parse({rel: 'prev', href: 'courses/?offset=20&limit=10'}, client) + links = described_class.parse({rel: 'prev', href: 'courses/?offset=20&limit=10'}, parse_context) expect(links.next).not_to be expect(links.prev.link_href).to eq 'courses/?offset=20&limit=10' expect(links.prev.link_rel).to eq 'prev' @@ -42,7 +43,7 @@ end it 'injects http client' do - links = described_class.parse({rel: 'next', href: 'courses/?offset=20&limit=10'}, client) + links = described_class.parse({rel: 'next', href: 'courses/?offset=20&limit=10'}, parse_context) expect(client).to receive(:send_request) links.next.follow end From 0e33df0095de55c24b4d6f192ebbc02462c832b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pol=C3=ADvka?= Date: Sun, 8 Mar 2015 23:53:58 +0100 Subject: [PATCH 4/6] Mocking fix refactored Less hackier way to fix mocking invoking `to_str` method which resulted to raising error with missing client even from separeted units of API client (as signal was bubbling to top module) --- lib/kosapi_client/kosapi_client.rb | 40 ++++++++++++++---------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/lib/kosapi_client/kosapi_client.rb b/lib/kosapi_client/kosapi_client.rb index 086ba19..9ac28b8 100644 --- a/lib/kosapi_client/kosapi_client.rb +++ b/lib/kosapi_client/kosapi_client.rb @@ -1,48 +1,44 @@ module KOSapiClient + DEFAULT_KOSAPI_BASE_URL = 'https://kosapi.fit.cvut.cz/api/3' + singleton_class.class_eval do - def new(options = {}) - ApiClient.new(Configuration.new(options)) + attr_reader :client + + alias_method :to_str, :to_s + + def new(credentials, base_url = DEFAULT_KOSAPI_BASE_URL) + http_adapter = OAuth2HttpAdapter.new(credentials, base_url) + http_client = HTTPClient.new(http_adapter) + ApiClient.new(http_client, base_url) end def configure - reset + config = Configuration.new yield config - self - end - - def client - @client ||= ApiClient.new(config) + @client = new(config.credentials) end # Calling this method clears stored ApiClient instance # if configured previously. def reset - @config = nil @client = nil end def method_missing(method, *args, &block) - if client.respond_to?(method) - client.send(method, *args, &block) + if @client.nil? + raise "Client not configured. Either you forgot to call configure or you have typo in method name '#{method}'." + end + if @client.respond_to?(method) + @client.send(method, *args, &block) else super end end def respond_to_missing?(method_name, include_private = false) - client.respond_to?(method_name, include_private) - end - - private - def config - @config ||= Configuration.new - end - - # Was interfering with mocking - def to_str - "KOSapi client" + @client.respond_to?(method_name, include_private) end end end From c30b363b309d421fee5d44d4e5dfb2c6d43e8a99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pol=C3=ADvka?= Date: Sun, 12 Apr 2015 00:23:19 +0200 Subject: [PATCH 5/6] Fixed bad merge --- lib/kosapi_client/api_client.rb | 2 +- lib/kosapi_client/kosapi_client.rb | 39 +++++++++++++++--------------- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/lib/kosapi_client/api_client.rb b/lib/kosapi_client/api_client.rb index 8252f36..e4f9c72 100644 --- a/lib/kosapi_client/api_client.rb +++ b/lib/kosapi_client/api_client.rb @@ -37,4 +37,4 @@ def find_builder_class(builder_name) end end -end +end \ No newline at end of file diff --git a/lib/kosapi_client/kosapi_client.rb b/lib/kosapi_client/kosapi_client.rb index 9ac28b8..b8e1a6a 100644 --- a/lib/kosapi_client/kosapi_client.rb +++ b/lib/kosapi_client/kosapi_client.rb @@ -1,44 +1,45 @@ module KOSapiClient - DEFAULT_KOSAPI_BASE_URL = 'https://kosapi.fit.cvut.cz/api/3' - singleton_class.class_eval do - attr_reader :client - - alias_method :to_str, :to_s - - def new(credentials, base_url = DEFAULT_KOSAPI_BASE_URL) - http_adapter = OAuth2HttpAdapter.new(credentials, base_url) - http_client = HTTPClient.new(http_adapter) - ApiClient.new(http_client, base_url) + def new(options = {}) + ApiClient.new(Configuration.new(options)) end def configure - config = Configuration.new + reset yield config - @client = new(config.credentials) + self + end + + def client + @client ||= ApiClient.new(config) end # Calling this method clears stored ApiClient instance # if configured previously. def reset + @config = nil @client = nil end def method_missing(method, *args, &block) - if @client.nil? - raise "Client not configured. Either you forgot to call configure or you have typo in method name '#{method}'." - end - if @client.respond_to?(method) - @client.send(method, *args, &block) + if client.respond_to?(method) + client.send(method, *args, &block) else super end end def respond_to_missing?(method_name, include_private = false) - @client.respond_to?(method_name, include_private) + client.respond_to?(method_name, include_private) end + + private + def config + @config ||= Configuration.new + end + end -end + +end \ No newline at end of file From b563c2d83a2ee2515caac893ea8569c8912f03bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Pol=C3=ADvka?= Date: Sat, 11 Apr 2015 22:51:52 +0200 Subject: [PATCH 6/6] Fixed (parallel of TV does not have any timetable slot) --- spec/integration/parallels_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/integration/parallels_spec.rb b/spec/integration/parallels_spec.rb index 848d3c2..87e4600 100644 --- a/spec/integration/parallels_spec.rb +++ b/spec/integration/parallels_spec.rb @@ -50,7 +50,7 @@ end it 'parses timetable slot ID' do - page = client.parallels + page = client.parallels.query('course.code' => 'MI-PAA') slot = page.items.first.timetable_slots.first expect(slot.id).not_to be_nil end