Skip to content

Commit

Permalink
It works
Browse files Browse the repository at this point in the history
  • Loading branch information
tkawa committed Sep 8, 2016
1 parent 4a53965 commit ea696e2
Show file tree
Hide file tree
Showing 7 changed files with 324 additions and 5 deletions.
15 changes: 12 additions & 3 deletions faraday-hypermedia.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ Gem::Specification.new do |spec|
spec.authors = ["Toru KAWAMURA"]
spec.email = ["tkawa@4bit.net"]

spec.summary = %q{TODO: Write a short summary, because Rubygems requires one.}
spec.description = %q{TODO: Write a longer description or delete this line.}
spec.homepage = "TODO: Put your gem's website or public repo URL here."
spec.summary = %q{Faraday middleware that supports hypermedia client}
spec.description = %q{Faraday middleware that supports hypermedia client}
spec.homepage = "https://github.com/tkawa/faraday-hypermedia"
spec.license = "MIT"

# Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
Expand All @@ -27,7 +27,16 @@ Gem::Specification.new do |spec|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths = ["lib"]

spec.add_dependency "faraday", ">= 0.8.0"
spec.add_dependency 'faraday_middleware'
spec.add_dependency 'http_link_header'
spec.add_dependency 'faraday_collection_json'
spec.add_dependency 'uri_template'

spec.add_development_dependency "bundler", "~> 1.11"
spec.add_development_dependency "rake", "~> 10.0"
spec.add_development_dependency "minitest", "~> 5.0"
spec.add_development_dependency 'awesome_print'
spec.add_development_dependency 'faraday-detailed_logger'
spec.add_development_dependency 'faraday-http-cache'
end
1 change: 1 addition & 0 deletions lib/faraday-hypermedia.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require 'faraday/hypermedia'
14 changes: 12 additions & 2 deletions lib/faraday/hypermedia.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
require "faraday/hypermedia/version"
require 'faraday'
require 'faraday/hypermedia/history'
require 'faraday/hypermedia/link_extractor'
require 'faraday/hypermedia/navigation'
require 'faraday/hypermedia/version'
require_relative '../uri/navigation'

module Faraday
module Hypermedia
# Your code goes here...
RE_URI_TEMPLATE = /\{.*\}/
end

Middleware.register_middleware navigation: Hypermedia::Navigation
Response.register_middleware link_cj: Hypermedia::LinkExtractorCJ
Response.register_middleware link_github: Hypermedia::LinkExtractorGithub

end
120 changes: 120 additions & 0 deletions lib/faraday/hypermedia/history.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
require 'http_link_header'
require 'uri_template'

module Faraday
module Hypermedia
class History
State = Struct.new(:data, :title, :url)

def initialize
@stored_states = [build_state]
@current_index = 0
reset_current_links
end

def current_state
@stored_states[@current_index]
end

# いったんここに実装
def current_links
@current_links ||= build_current_links
end

def pp_current_links
# TODO: きれいにする
# current_links.to_a.join("#{current_links.class::DELIMETER}\n")
rels = current_links.map{|url, params| params['rel'].to_a.map{|rel| [rel, [url, params]] }}.flatten(1).group_by{|rel, _| rel}
rels.each do |rel, links|
puts("rel=#{rel}")
if links.length >= 2
links.each.with_index(1) do |(_, link), i|
url, params = link
params = params.dup
params.delete('rel')
print(" (#{i}) <#{url}>")
print(" (#{params})") unless params.empty?
puts
end
else
url, params = links.first.last
params = params.dup
params.delete('rel')
print(" <#{url}>")
print(" (#{params})") unless params.empty?
puts
end
end
nil
end

def fill_in_template_params(template_params)
template_urls = current_links.keys.select { |k| k =~ RE_URI_TEMPLATE }
template_urls.each do |template_url|
expanded_url = URITemplate.new(template_url).expand(template_params) # Addressable のほうが良いかも
link_params = current_links.delete(template_url)
current_links.store(expanded_url, link_params)
end
end

def reset_current_links
@current_links = nil
end

def push_state(data, title, url)
if url == current_state.url
replace_state(data, title, url)
else
@stored_states.slice!((@current_index+1)..-1) # current_index の先から最後まで削除
state = build_state(data: data, title: title, url: url)
@stored_states.push(state)
@current_index += 1
reset_current_links
state
end
end

def replace_state(data, title, url)
reset_current_links
@stored_states[@current_index] = build_state(data: data, title: title, url: url)
end

def push(response_env)
url = response_env[:url]
push_state(response_env, '', url)
end

def back
if @current_index >= 1
@current_index -= 1
reset_current_links
end
end

def forward
if @current_index < @stored_states.length - 1
@current_index += 1
reset_current_links
end
end

private

def build_state(source = {})
State.new(
source[:data] || {},
source[:title] || '',
source[:url] || URI('navigation:blank')
)
end

def build_current_links
current_response_headers = current_state.data[:response_headers]
return HttpLinkHeader.new unless current_response_headers
links = current_response_headers['link'].to_s.scan(/<[^>]*>[^,]*/)
link_templates = current_response_headers['link-template'].to_s.scan(/<[^>]*>[^,]*/) # 自前でsplitする
HttpLinkHeader.new(links + link_templates)
end
end
end
end
88 changes: 88 additions & 0 deletions lib/faraday/hypermedia/link_extractor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
require 'faraday_collection_json'
require 'faraday_middleware'

module Faraday
module Hypermedia
class LinkExtractorCJ < FaradayCollectionJSON::ParseCollectionJSON
def process_response(response_env)
# TODO: collectionのlinksしか取り出していない。itemsにも存在する
# TODO: itemsのselfがあれば取り出す
cj = parse(response_env[:body])
return if cj.links.empty? && cj.items.empty?
# link_arrays = cj.links.map do |link|
# attrs = [['rel', link.rel]]
# attrs << ['title', link.name] if link.name
# [link.href, attrs]
# end
# link_value = LinkHeader.new(link_arrays).to_s
link_values = cj.links.map { |link|
value = %(<#{link.href}>;rel="#{link.rel}")
value += %(;title="#{link.name}") if link.name
value
}
item_values = cj.items.map { |item| %(<#{item.href}>;rel="item") if item.href }.compact
link_header = (link_values + item_values).join(',')
if response_env[:response_headers]['link']
response_env[:response_headers]['link'] += ",#{link_header}"
else
response_env[:response_headers]['link'] = link_header
end
end
end

class LinkExtractorGithub < FaradayMiddleware::ParseJson
def process_response(response_env)
doc = parse(response_env[:body])
if doc.is_a? Hash
link_template_props, link_props = doc.select { |name, _| name.end_with?('_url') }.partition { |_, url| url =~ RE_URI_TEMPLATE }
self_url = doc['url']
return if link_props.empty? && self_url.nil?
link_values = build_link_values(link_props)
link_values.unshift(%(<#{self_url}>;rel="self")) if self_url
item_props = doc.map { |name, c| [name, c['url']] if c.is_a?(Hash) && c['url'] }.compact
link_values.concat build_item_link_values(item_props)
link_template_values = build_link_values(link_template_props)
elsif doc.is_a? Array
# treat it as a collection
item_props = doc.map { |d| [d['name'], d['url']] if d['url'] }.compact
return if item_props.empty?
link_values = build_item_link_values(item_props)
link_template_values = []
else
return
end
link_header = link_values.join(',')
unless link_header.empty?
if response_env[:response_headers]['link']
response_env[:response_headers]['link'] += ",#{link_header}"
else
response_env[:response_headers]['link'] = link_header
end
end
link_template_header = link_template_values.join(',')
unless link_template_header.empty?
if response_env[:response_headers]['link-template']
response_env[:response_headers]['link-template'] += ",#{link_template_header}"
else
response_env[:response_headers]['link-template'] = link_template_header
end
end
end

private

def build_link_values(props)
props.map { |name, url|
rel = name.slice(0..-5) # '_url' を除いた名前
%(<#{url}>;rel="#{rel}") if url.to_s != ''
}.compact
end

def build_item_link_values(props)
props.map { |name, url|
%(<#{url}>;rel="item";title="#{name}") if url.to_s != ''
}.compact
end
end
end
end
52 changes: 52 additions & 0 deletions lib/faraday/hypermedia/navigation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
module Faraday
module Hypermedia
class Navigation < Middleware
attr_reader :history

def initialize(app, history = nil, options = {})
super(app)
puts 'Navigation Enabled!'
@history = history || History.new
# state = self
# connection.define_singleton_method(:history) do
# state.history
# end
end

def call(request_env)
# request
url = request_env[:url]
if url.scheme == 'navigation'
case url.to
when 'back'
@history.back
request_env[:url] = @history.current_state.url
when 'forward'
@history.forward
request_env[:url] = @history.current_state.url
when 'go'
# TODO
when /\Alink(?:\((\d+)\))?\z/ # link or link(index)
index = (Regexp.last_match(1) || 1).to_i # one origin
matched_links = @history.current_links
unless url.queries.empty?
attr_name, attr_value = url.queries.first # TODO: multiple
matched_links = matched_links.select { |_ ,v| v[attr_name] && v[attr_name].include?(attr_value) } # TODO: multiple
end
raise 'cannot find link' if matched_links.empty?
matched_url = matched_links.to_a[index - 1].first
matched_url = URITemplate.new(matched_url).expand if matched_url =~ RE_URI_TEMPLATE
request_env[:url] = URI(matched_url)
else
raise "cannot use #{url.to}"
end
end

# response
@app.call(request_env).on_complete do |response_env|
@history.push response_env
end
end
end
end
end
39 changes: 39 additions & 0 deletions lib/uri/navigation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
require 'uri'

module URI
class Navigation < Generic
DEFAULT_PORT = nil
COMPONENT = [ :scheme, :to, :query ].freeze

def initialize(*arg)
super(*arg)
to, query = @opaque.split('?', 2)
@to = to
self.query = query
end

attr_reader :to

def query=(v)
return @query = nil unless v

x = v.to_str
v = x.dup if x.equal? v
v.encode!(Encoding::UTF_8) rescue nil
v.delete!("\t\r\n")
v.force_encoding(Encoding::ASCII_8BIT)
v.gsub!(/(?!%\h\h|[!$-&(-;=?-_a-~])./n.freeze){'%%%02X' % $&.ord}
v.force_encoding(Encoding::US_ASCII)
@query = v
end

def queries
@query.split('&').map { |x| x.split(/=/, 2) }.to_h
end

def to_s
"#{@scheme}:#{@to}" + (@query && !@query.empty? ? "?#{@query}" : '')
end
end
@@schemes['NAVIGATION'] = Navigation
end

0 comments on commit ea696e2

Please sign in to comment.