ahora 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
data/README.md ADDED
@@ -0,0 +1,117 @@
1
+ Ahora
2
+ =====
3
+
4
+ An alternative to ActiveResource for consuming Java-ish XML HTTP Resources easily. *Ahora* glues together two amazing Ruby libraries: [Faraday](https://github.com/technoweenie/faraday) and [Nibbler](https://github.com/mislav/nibbler).
5
+
6
+ If your project meets any of the following requirements, you might be interested.
7
+
8
+ * You want to consume an external XML resource.
9
+ * You want to map camelCase names to underscore_case.
10
+ * You want to parse Date, Time and boolean values automatically.
11
+ * You want to be a good citizen on the web and support caching (e.g. If-Modified-Since).
12
+ * You want to use fragment caching on the front-end. So it should generate a cache-key.
13
+ * You might have a recently cached fragment, so XML collections should be lazy loaded.
14
+
15
+ This is a big list, you might not need all these requirements. At least the code can act as an example on how to approach one of these problems.
16
+
17
+ Example
18
+ ---
19
+
20
+ Let's say you want to display to following XML on a page in a Rails project
21
+
22
+ ~~~ xml
23
+ <?xml version="1.0"?>
24
+ <userPosts type="array">
25
+ <userPost>
26
+ <hidden>false</hidden>
27
+ <objectId>1</objectId>
28
+ <userObjectId>1</userObjectId>
29
+ <user>
30
+ <firstName>John</firstName>
31
+ <lastName>Doe</lastName>
32
+ </user>
33
+ <body>How is everybody today?</body>
34
+ <createdAt>2011-10-26T17:01:52+02:00</createdAt>
35
+
36
+ <replies type="array">
37
+ <userPost>
38
+ <objectId>2</objectId>
39
+ <parentObjectId>1</parentObjectId>
40
+ <userObjectId>2</userObjectId>
41
+ <user>
42
+ <firstName>Mike</firstName>
43
+ <lastName>Smith</lastName>
44
+ </user>
45
+ <body>I am fine, thanks for asking.</body>
46
+ <createdAt>2011-10-27T9:00:00+02:00</createdAt>
47
+ </userPost>
48
+ <userPost>
49
+ <objectId>3</objectId>
50
+ <parentObjectId>1</parentObjectId>
51
+ <userObjectId>1</userObjectId>
52
+ <user>
53
+ <firstName>John</firstName>
54
+ <lastName>Doe</lastName>
55
+ </user>
56
+ <body>You are more than welcome.</body>
57
+ <createdAt>2011-10-27T9:00:00+02:00</createdAt>
58
+ </userPost>
59
+ </replies>
60
+ </userPost>
61
+ <userPost>
62
+ <hidden>true</hidden>
63
+ </userPost>
64
+ </userPosts>
65
+ ~~~
66
+
67
+ You start by defining a class with methods to retrieve the resource and another class to act as a mapper.
68
+
69
+ ~~~ ruby
70
+ class Post < Ahora::Representation
71
+ objectid :id, :user_id, :parent_id
72
+ date :created_at
73
+ element :body
74
+ element 'user', :with => Ahora::Representation do
75
+ string :first_name, :last_name
76
+ end
77
+ boolean :hidden
78
+ elements 'replies/userPost' => :replies, :with => Post
79
+ end
80
+
81
+ class PostResource
82
+ include Ahora::Resource
83
+
84
+ def all
85
+ collection Post, get("/api/posts.xml")
86
+ end
87
+
88
+ private
89
+ def host
90
+ 'http://test.net'
91
+ end
92
+
93
+ def extend_middleware(builder)
94
+ builder.use Ahora::Middleware::LastModifiedCaching, Rails.cache
95
+ end
96
+ end
97
+ ~~~
98
+
99
+ Now you can define a controller as usual.
100
+
101
+ ~~~ ruby
102
+ class PostsController < ApplicationController::Base
103
+ def index
104
+ @posts = PostResource.new.all
105
+ end
106
+ end
107
+ ~~~
108
+
109
+ And a view template
110
+
111
+ ~~~ html
112
+ <% cache @posts, 'index' %>
113
+ <%= render(:partial => @posts) %>
114
+ <% end >
115
+ ~~~
116
+
117
+ And that's all there is. The XML response will be cached, so it saves a bandwith. The XML response will only be parsed if there is no existing HTML fragment cache. All cache will be invalidated when the request to posts.xml returns a new body instead of a 304 Not Modified.
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require 'rake/testtask'
2
+ require 'bundler'
3
+ Bundler::GemHelper.install_tasks
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.pattern = 'test/**/*_test.rb'
7
+ end
data/ahora.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "ahora/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "ahora"
7
+ s.version = Ahora::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Matthijs Langenberg"]
10
+ s.email = ["matthijs.langenberg@nedap.com"]
11
+ s.homepage = ""
12
+ s.summary = %q{Consume Java-ish XML HTTP Resources easily}
13
+ s.description = %q{Consume Java-ish XML HTTP Resources easily}
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files -- {test}/*`.split("\n")
17
+ #s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
+ s.require_paths = ["lib"]
19
+
20
+ s.add_dependency "nibbler", '>= 1.3.0'
21
+ s.add_dependency "faraday", '>= 0.7'
22
+ s.add_dependency "nokogiri", "~> 1.5"
23
+ s.add_dependency "activesupport"
24
+
25
+ s.add_development_dependency "fakeweb"
26
+ s.add_development_dependency "minitest"
27
+ end
@@ -0,0 +1,88 @@
1
+ module Ahora
2
+ module Middleware
3
+ class LastModifiedCaching < Faraday::Middleware
4
+ attr_reader :cache
5
+
6
+ extend Forwardable
7
+ def_delegators :'Faraday::Utils', :parse_query, :build_query
8
+
9
+ # Public: initialize the middleware.
10
+ #
11
+ # cache - An object that responds to read, write and fetch (default: nil).
12
+ # options - An options Hash (default: {}):
13
+ # :ignore_params - String name or Array names of query params
14
+ # that should be ignored when forming the cache
15
+ # key (default: []).
16
+ # :cache_header - String name of response header that should be
17
+ # used to retrieve cache timestamp from.
18
+ # Defaults to 'last-modified'
19
+ # :expire_in - Integer time to live value in seconds for a key.
20
+ # Defaults to never expire.
21
+ #
22
+ # Yields if no cache is given. The block should return a cache object.
23
+ def initialize(app, cache = nil, options = {})
24
+ super(app)
25
+ options, cache = cache, nil if cache.is_a? Hash and block_given?
26
+ @cache = cache || yield
27
+ @options = options
28
+ end
29
+
30
+ def call(env)
31
+ if env[:method] == :get
32
+
33
+ timestamp_key = cache_key(env) + ':timestamp'
34
+ data_key = cache_key(env) + ':response'
35
+
36
+ if date = cache.read(timestamp_key)
37
+ # WARN FakeWeb cannot test this
38
+ env[:request_headers]['If-Modified-Since'] = Time.parse(date).httpdate
39
+ end
40
+
41
+ response = @app.call(env)
42
+
43
+ if response.status == 304
44
+ response = cache.read data_key
45
+ elsif date = response.headers[@options[:cache_header] || 'Last-Modified']
46
+ response.env[:response_headers]['X-Ahora-Cache-Key'] = fragment_cache_key(env, date)
47
+ cache.write timestamp_key, date, :expire_in => @options[:expire_in]
48
+ cache.write data_key, response, :expire_in => @options[:expire_in]
49
+ end
50
+
51
+ finalize_response(response, env)
52
+ else
53
+ @app.call(env)
54
+ end
55
+ end
56
+
57
+ def fragment_cache_key(env, last_modified)
58
+ cache_key(env) + ":fragment_#{last_modified}"
59
+ end
60
+
61
+ def cache_key(env)
62
+ url = env[:url].dup
63
+ if url.query && params_to_ignore.any?
64
+ params = parse_query url.query
65
+ params.reject! {|k,| params_to_ignore.include? k }
66
+ url.query = build_query params
67
+ end
68
+ url.normalize!
69
+ url.to_s
70
+ end
71
+
72
+ def params_to_ignore
73
+ @params_to_ignore ||= Array(@options[:ignore_params]).map { |p| p.to_s }
74
+ end
75
+
76
+ def finalize_response(response, env)
77
+ response = response.dup if response.frozen?
78
+ env[:response] = response
79
+ unless env[:response_headers]
80
+ env.update response.env
81
+ # FIXME: omg hax
82
+ response.instance_variable_set('@env', env)
83
+ end
84
+ response
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,85 @@
1
+ module Ahora
2
+ module Middleware
3
+ class RequestLogger < Faraday::Response::Middleware
4
+ def initialize(app, logger)
5
+ super(app)
6
+ @logger = logger || begin
7
+ require 'logger'
8
+ Logger.new(STDOUT)
9
+ end
10
+ end
11
+
12
+ def call(env)
13
+ @logger.info "#{env[:method].to_s.upcase} #{env[:url].to_s}"
14
+ @started = Time.now
15
+ super
16
+ end
17
+
18
+ def on_complete(env)
19
+ duration = 1000.0 * (Time.now - @started)
20
+ kbytes = env[:body].to_s.length / 1024.0
21
+ @logger.info "--> %d %s %.2fKB (%.1fms)" % [env[:status], HTTP_STATUS_CODES[env[:status]], kbytes, duration]
22
+ end
23
+
24
+ # Every standard HTTP code mapped to the appropriate message.
25
+ # Generated with:
26
+ # curl -s http://www.iana.org/assignments/http-status-codes | \
27
+ # ruby -ane 'm = /^(\d{3}) +(\S[^\[(]+)/.match($_) and
28
+ # puts " #{m[1]} => \x27#{m[2].strip}x27,"'
29
+ HTTP_STATUS_CODES = {
30
+ 100 => 'Continue',
31
+ 101 => 'Switching Protocols',
32
+ 102 => 'Processing',
33
+ 200 => 'OK',
34
+ 201 => 'Created',
35
+ 202 => 'Accepted',
36
+ 203 => 'Non-Authoritative Information',
37
+ 204 => 'No Content',
38
+ 205 => 'Reset Content',
39
+ 206 => 'Partial Content',
40
+ 207 => 'Multi-Status',
41
+ 226 => 'IM Used',
42
+ 300 => 'Multiple Choices',
43
+ 301 => 'Moved Permanently',
44
+ 302 => 'Found',
45
+ 303 => 'See Other',
46
+ 304 => 'Not Modified',
47
+ 305 => 'Use Proxy',
48
+ 306 => 'Reserved',
49
+ 307 => 'Temporary Redirect',
50
+ 400 => 'Bad Request',
51
+ 401 => 'Unauthorized',
52
+ 402 => 'Payment Required',
53
+ 403 => 'Forbidden',
54
+ 404 => 'Not Found',
55
+ 405 => 'Method Not Allowed',
56
+ 406 => 'Not Acceptable',
57
+ 407 => 'Proxy Authentication Required',
58
+ 408 => 'Request Timeout',
59
+ 409 => 'Conflict',
60
+ 410 => 'Gone',
61
+ 411 => 'Length Required',
62
+ 412 => 'Precondition Failed',
63
+ 413 => 'Request Entity Too Large',
64
+ 414 => 'Request-URI Too Long',
65
+ 415 => 'Unsupported Media Type',
66
+ 416 => 'Requested Range Not Satisfiable',
67
+ 417 => 'Expectation Failed',
68
+ 418 => "I'm a Teapot",
69
+ 422 => 'Unprocessable Entity',
70
+ 423 => 'Locked',
71
+ 424 => 'Failed Dependency',
72
+ 426 => 'Upgrade Required',
73
+ 500 => 'Internal Server Error',
74
+ 501 => 'Not Implemented',
75
+ 502 => 'Bad Gateway',
76
+ 503 => 'Service Unavailable',
77
+ 504 => 'Gateway Timeout',
78
+ 505 => 'HTTP Version Not Supported',
79
+ 506 => 'Variant Also Negotiates',
80
+ 507 => 'Insufficient Storage',
81
+ 510 => 'Not Extended',
82
+ }
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,3 @@
1
+ %w( last_modified_caching request_logger ).each do |component|
2
+ require File.join(File.expand_path(File.dirname(__FILE__)), 'middleware', component)
3
+ end
@@ -0,0 +1,122 @@
1
+ require 'nibbler'
2
+ require 'nokogiri'
3
+ require 'date'
4
+ require 'active_support/core_ext/string'
5
+ require 'delegate'
6
+
7
+ module Ahora
8
+ class Representation < Nibbler
9
+ INTEGER_PARSER = lambda { |node| Integer(node.content) if node.content.present? }
10
+ DATE_PARSER = lambda { |node| Date.parse(node.content) if node.content.present? }
11
+ TIME_PARSER = lambda { |node| Time.parse(node.content) if node.content.present? }
12
+ BOOL_PARSER =
13
+ lambda { |node| node.content.to_s.downcase == 'true' if node.content.present? }
14
+
15
+ module Definition
16
+ def element(*)
17
+ name = super
18
+ define_method "#{name}?" do
19
+ !!instance_variable_get("@#{name}")
20
+ end
21
+ name
22
+ end
23
+
24
+ def attribute(*names)
25
+ names = names.flatten
26
+ parser = names.pop if names.last.is_a?(Proc)
27
+ names.each do |name|
28
+ selector = name
29
+ if name.is_a? Hash
30
+ selector, name = name.first
31
+ end
32
+ element selector.to_s.camelcase(:lower) => name.to_s, :with => parser
33
+ end
34
+ end
35
+
36
+ # Public: define Java style object id mapping
37
+ #
38
+ # *names - Array of String or Symbol ruby style names
39
+ #
40
+ # Examples
41
+ #
42
+ # objectid :id, parent_id
43
+ # # is equivalent to
44
+ # element 'objectId' => 'id'
45
+ # element 'parentObjectId' => 'parent_id'
46
+ def objectid(*names)
47
+ attribute names.map { |name| { name.to_s.gsub('id', 'object_id') => name } }, INTEGER_PARSER
48
+ end
49
+
50
+ def string(*names)
51
+ attribute(names)
52
+ end
53
+
54
+ def integer(*names)
55
+ attribute(names, INTEGER_PARSER)
56
+ end
57
+
58
+ def date(*names)
59
+ attribute(names, DATE_PARSER)
60
+ end
61
+
62
+ # FIXME test
63
+ def time(*names)
64
+ attribute(names, TIME_PARSER)
65
+ end
66
+
67
+ def boolean(*names)
68
+ attribute(names, BOOL_PARSER)
69
+ end
70
+ end
71
+
72
+ extend Definition
73
+
74
+ def initialize(doc_or_atts = {})
75
+ if doc_or_atts.is_a? Hash
76
+ super(nil)
77
+ doc_or_atts.each do |key, val|
78
+ send("#{key}=", val)
79
+ end
80
+ else
81
+ super
82
+ end
83
+ end
84
+ end
85
+
86
+ class Collection < DelegateClass(Array)
87
+ NoCacheKeyAvailable = Class.new(StandardError)
88
+ def initialize(instantiator, document_parser, response)
89
+ @instantiator = instantiator
90
+ @document_parser = document_parser
91
+ @response = response
92
+ @cache_key = response.env[:response_headers]['X-Ahora-Cache-Key']
93
+ super([])
94
+ end
95
+
96
+ def cache_key
97
+ @cache_key or raise NoCacheKeyAvailable,
98
+ "No caching middleware is used or resource does not support caching."
99
+ end
100
+
101
+ %w( to_s to_a size each first last [] inspect pretty_print ).each do |method_name|
102
+ eval "def #{method_name}(*); kicker; super; end"
103
+ end
104
+
105
+ private
106
+ def kicker
107
+ @_collection ||= __setobj__ doc.search("/*[@type='array']/*").map { |element|
108
+ @instantiator.call element
109
+ }.to_a.compact
110
+ end
111
+
112
+ def doc
113
+ @document_parser.call(@response.body)
114
+ end
115
+ end
116
+
117
+ class XmlParser
118
+ def self.parse(body)
119
+ Nokogiri::XML.parse(body, nil, nil, Nokogiri::XML::ParseOptions::STRICT)
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,68 @@
1
+ require 'faraday'
2
+
3
+ module Ahora
4
+ module Resource
5
+ attr_writer :document_parser
6
+
7
+ def get(url, params = {})
8
+ connection.get do |req|
9
+ set_common_headers(req)
10
+ req.url url, params
11
+ end
12
+ end
13
+
14
+ # FIXME test
15
+ def post(url, body)
16
+ connection.post do |req|
17
+ set_common_headers(req)
18
+ req.url url
19
+ req.body = body
20
+ end
21
+ end
22
+
23
+ # FIXME test
24
+ def put(url, body)
25
+ connection.put do |req|
26
+ set_common_headers(req)
27
+ req.url url
28
+ req.body = body
29
+ end
30
+ end
31
+
32
+ def connection
33
+ conn = Faraday.new(host, :ssl => { :verify => false }) do |builder|
34
+ builder.use Faraday::Response::RaiseError
35
+ extend_middleware(builder)
36
+ builder.adapter Faraday.default_adapter
37
+ end
38
+ conn.headers['User-Agent'] = 'Ahora'
39
+ conn
40
+ end
41
+
42
+ # @abstract override to use custom Faraday middleware
43
+ def extend_middleware(builder); end;
44
+
45
+ def collection(*args, &block)
46
+ if args.size == 2
47
+ klass, response = args
48
+ instantiator = lambda do |doc|
49
+ klass.parse(doc)
50
+ end
51
+ else
52
+ response = args.first
53
+ instantiator = block
54
+ end
55
+ Collection.new instantiator, document_parser, response
56
+ end
57
+
58
+ private
59
+ def document_parser
60
+ @document_parser ||= XmlParser.method(:parse)
61
+ end
62
+
63
+ def set_common_headers(req)
64
+ req.headers['Content-Type'] = 'application/xml'
65
+ req.headers['Accept'] = 'application/xml'
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,3 @@
1
+ module Ahora
2
+ VERSION = "0.0.1"
3
+ end
data/lib/ahora.rb ADDED
@@ -0,0 +1,3 @@
1
+ %w( resource representation middleware ).each do |component|
2
+ require File.join(File.expand_path(File.dirname(__FILE__)), 'ahora', component)
3
+ end
@@ -0,0 +1,39 @@
1
+ require 'minitest/autorun'
2
+ require 'minitest/pride'
3
+ require_relative '../test_helper'
4
+ require_relative '../../lib/ahora/representation'
5
+
6
+ class Employee < Ahora::Representation
7
+ boolean :is_rockstar, :slacker, :fired
8
+ end
9
+
10
+ describe "boolean elements" do
11
+ let(:employee) { Employee.parse(fixture('employee').read) }
12
+
13
+ it "parsing 'true' adds the correct reader" do
14
+ employee.is_rockstar.must_equal true
15
+ employee.is_rockstar = false
16
+ employee.is_rockstar.must_equal false
17
+ employee.is_rockstar?.must_equal false
18
+ end
19
+
20
+ it "parsing 'false' adds the correct reader" do
21
+ employee.slacker.must_equal false
22
+ end
23
+
24
+ it "parsing missing value adds the correct reader" do
25
+ employee.fired.must_equal nil
26
+ end
27
+
28
+ it "parsing 'true' adds the correct question mark reader" do
29
+ employee.is_rockstar?.must_equal true
30
+ end
31
+
32
+ it "parsing 'false' adds the correct question mark reader" do
33
+ employee.slacker?.must_equal false
34
+ end
35
+
36
+ it "parsing missing value adds the correct question mark reader" do
37
+ employee.fired?.must_equal false
38
+ end
39
+ end
@@ -0,0 +1,214 @@
1
+ require 'fakeweb'
2
+ require 'minitest/autorun'
3
+ require 'minitest/pride'
4
+ require_relative 'test_helper'
5
+ require_relative '../lib/ahora'
6
+
7
+ FakeWeb.allow_net_connect = false
8
+
9
+ require 'singleton'
10
+ class MemCache
11
+ include Singleton
12
+ def initialize
13
+ @store = {}
14
+ end
15
+
16
+ def read(key)
17
+ @store[key] ? Marshal.load(@store[key]) : nil
18
+ end
19
+
20
+ def write(key, value, options = {})
21
+ @store[key] = Marshal.dump(value)
22
+ end
23
+ end
24
+
25
+ class PostRepository
26
+ include Ahora::Resource
27
+ USERNAME = 'user'
28
+ PASSWORD = 'pass'
29
+
30
+ def find_by_user_id(id)
31
+ collection Post, get("/users/#{id}/posts.xml")
32
+ end
33
+
34
+ def extend_middleware(builder)
35
+ builder.use Faraday::Request::BasicAuthentication, USERNAME, PASSWORD
36
+ builder.use Ahora::Middleware::LastModifiedCaching, MemCache.instance
37
+ end
38
+
39
+ def host
40
+ 'http://test.net/'
41
+ end
42
+ end
43
+
44
+ class Post < Ahora::Representation
45
+ objectid :id, :user_id, :parent_id
46
+ date :created_at
47
+ element :body
48
+ element 'user', :with => Ahora::Representation do
49
+ string :first_name, :last_name
50
+ end
51
+ boolean :hidden
52
+ elements 'replies/userPost' => :replies, :with => Post
53
+ end
54
+
55
+ class PostDomainRepository < PostRepository
56
+ def find_by_user_id(id)
57
+ collection PostDomain, get("/users/#{id}/posts.xml")
58
+ end
59
+ end
60
+
61
+ class PostDomain < DelegateClass(Post)
62
+ def self.parse(doc)
63
+ post = Post.parse(doc)
64
+ post.hidden? ? nil : new(post)
65
+ end
66
+
67
+ def initialize(post)
68
+ super(post)
69
+ end
70
+
71
+ def published?
72
+ created_at < Date.today
73
+ end
74
+ end
75
+
76
+ class BlogPostRepository < PostRepository
77
+ include Ahora::Resource
78
+
79
+ def all
80
+ collection get("/users/1/posts.xml") do |doc|
81
+ Post.parse(doc)
82
+ end
83
+ end
84
+ end
85
+
86
+ describe "requesting a collection" do
87
+ before do
88
+ fake_http '/users/1/posts.xml', fixture('user_posts')
89
+ @posts = PostRepository.new.find_by_user_id(1)
90
+ end
91
+
92
+ it "has the right size" do
93
+ @posts.size.must_equal 2
94
+ end
95
+
96
+ it "raises on #cache_key, because the backend does not has support for it" do
97
+ -> { @posts.cache_key }.must_raise(Ahora::Collection::NoCacheKeyAvailable)
98
+ end
99
+
100
+ describe "a single post from the collection" do
101
+ subject { @posts.first }
102
+
103
+ it "has the right body" do
104
+ subject.body.must_equal "How is everybody today?"
105
+ end
106
+
107
+ it "generates a questionmark method" do
108
+ subject.body?.must_equal true
109
+ end
110
+
111
+ it "renames foreign element names and converts integers" do
112
+ subject.id.must_equal 1
113
+ subject.user_id.must_equal 1
114
+ end
115
+
116
+ it "must handle date conversion" do
117
+ subject.created_at.must_equal Date.parse("2011-10-26T17:01:52+02:00")
118
+ end
119
+
120
+ it "handles nested resources" do
121
+ subject.user.first_name.must_equal "John"
122
+ end
123
+
124
+ it "handles nested collection resources" do
125
+ subject.replies.size.must_equal 2
126
+ end
127
+
128
+ describe "a single reply" do
129
+ let(:reply) { subject.replies.first }
130
+
131
+ it "behaves the same as a single post" do
132
+ reply.id.must_equal 2
133
+ reply.user_id.must_equal 2
134
+ reply.parent_id.must_equal 1
135
+ reply.user.last_name.must_equal "Smith"
136
+ end
137
+ end
138
+ end
139
+ end
140
+
141
+ describe 'requesting a collection with if-modified-since support' do
142
+ before do
143
+ FakeWeb.register_uri :get, uri('/users/1/posts.xml'), [
144
+ { :body => fixture('user_posts'), 'Last-Modified' => 'Mon, 02 Apr 2012 15:20:41 GMT' },
145
+ { :body => nil, :status => [304, 'Not Modified'] }
146
+ ]
147
+ @repository = PostRepository.new
148
+ end
149
+
150
+ it "has a cache key" do
151
+ @posts = @repository.find_by_user_id(1)
152
+ @posts.cache_key.
153
+ must_equal 'http://test.net/users/1/posts.xml:fragment_Mon, 02 Apr 2012 15:20:41 GMT'
154
+ end
155
+
156
+ it "has a cache key for cached response" do
157
+ @repository.find_by_user_id(1).cache_key.
158
+ must_equal @repository.find_by_user_id(1).cache_key
159
+ end
160
+
161
+ it "caches when response header includes Last-Modified" do
162
+ @repository.find_by_user_id(1).size.must_equal(2)
163
+ @posts = @repository.find_by_user_id(1).size.must_equal(2)
164
+ end
165
+ end
166
+
167
+ describe 'lazy loading' do
168
+ before do
169
+ @repository = PostRepository.new
170
+ @repository.document_parser = -> body { raise('NotLazyEnough') }
171
+ end
172
+
173
+ it "should not parse the response if not necessary" do
174
+ fake_http '/users/1/posts.xml', fixture('user_posts')
175
+ @posts = PostRepository.new.find_by_user_id(1)
176
+ end
177
+
178
+ it "should work the same for domain layer type models" do
179
+ fake_http '/users/1/posts.xml', fixture('user_posts')
180
+ PostDomainRepository.new.find_by_user_id(1)
181
+ end
182
+ end
183
+
184
+ describe "creating a new instance" do
185
+ it "allows to create a new instance with an attribute hash" do
186
+ p = Post.new :body => 'hi'
187
+ p.body.must_equal 'hi'
188
+ end
189
+ end
190
+
191
+ describe "being wrapped by a domain layer" do
192
+ before do
193
+ fake_http '/users/1/posts.xml', fixture('user_posts')
194
+ @posts = PostDomainRepository.new.find_by_user_id(1)
195
+ @post = @posts.first
196
+ end
197
+
198
+ it "handles a collection" do
199
+ @post.published?.must_equal true
200
+ @post.user.first_name.must_equal 'John'
201
+ end
202
+
203
+ it "allows filtering by letting the instantiator return nil" do
204
+ @posts.size.must_equal 1
205
+ end
206
+ end
207
+
208
+ describe "passing a block instead of a class to collection" do
209
+ it "returns the parsed document" do
210
+ fake_http '/users/1/posts.xml', fixture('user_posts')
211
+ repository = BlogPostRepository.new
212
+ repository.all.first.id.must_equal 1
213
+ end
214
+ end
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0"?>
2
+ <employee>
3
+ <firstName>John</firstName>
4
+ <isRockstar>true</isRockstar>
5
+ <slacker>false</slacker>
6
+ </employee>
@@ -0,0 +1,42 @@
1
+ <?xml version="1.0"?>
2
+ <userPosts type="array">
3
+ <userPost>
4
+ <hidden>false</hidden>
5
+ <objectId>1</objectId>
6
+ <userObjectId>1</userObjectId>
7
+ <user>
8
+ <firstName>John</firstName>
9
+ <lastName>Doe</lastName>
10
+ </user>
11
+ <body>How is everybody today?</body>
12
+ <createdAt>2011-10-26T17:01:52+02:00</createdAt>
13
+
14
+ <replies type="array">
15
+ <userPost>
16
+ <objectId>2</objectId>
17
+ <parentObjectId>1</parentObjectId>
18
+ <userObjectId>2</userObjectId>
19
+ <user>
20
+ <firstName>Mike</firstName>
21
+ <lastName>Smith</lastName>
22
+ </user>
23
+ <body>I am fine, thanks for asking.</body>
24
+ <createdAt>2011-10-27T9:00:00+02:00</createdAt>
25
+ </userPost>
26
+ <userPost>
27
+ <objectId>3</objectId>
28
+ <parentObjectId>1</parentObjectId>
29
+ <userObjectId>1</userObjectId>
30
+ <user>
31
+ <firstName>John</firstName>
32
+ <lastName>Doe</lastName>
33
+ </user>
34
+ <body>You are more than welcome.</body>
35
+ <createdAt>2011-10-27T9:00:00+02:00</createdAt>
36
+ </userPost>
37
+ </replies>
38
+ </userPost>
39
+ <userPost>
40
+ <hidden>true</hidden>
41
+ </userPost>
42
+ </userPosts>
@@ -0,0 +1,11 @@
1
+ def fake_http(path, body)
2
+ FakeWeb.register_uri :get, uri(path), :body => body
3
+ end
4
+
5
+ def uri(path)
6
+ 'http://user:pass@test.net' + path
7
+ end
8
+
9
+ def fixture(name)
10
+ File.open File.join(File.dirname('__FILE__'), 'test', 'fixtures', "#{name}.xml")
11
+ end
metadata ADDED
@@ -0,0 +1,128 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ahora
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Matthijs Langenberg
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-04-12 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: nibbler
16
+ requirement: &70160681904060 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 1.3.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70160681904060
25
+ - !ruby/object:Gem::Dependency
26
+ name: faraday
27
+ requirement: &70160681901920 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0.7'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70160681901920
36
+ - !ruby/object:Gem::Dependency
37
+ name: nokogiri
38
+ requirement: &70160681900040 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ version: '1.5'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *70160681900040
47
+ - !ruby/object:Gem::Dependency
48
+ name: activesupport
49
+ requirement: &70160681898560 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: *70160681898560
58
+ - !ruby/object:Gem::Dependency
59
+ name: fakeweb
60
+ requirement: &70160681897280 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *70160681897280
69
+ - !ruby/object:Gem::Dependency
70
+ name: minitest
71
+ requirement: &70160681896360 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: *70160681896360
80
+ description: Consume Java-ish XML HTTP Resources easily
81
+ email:
82
+ - matthijs.langenberg@nedap.com
83
+ executables: []
84
+ extensions: []
85
+ extra_rdoc_files: []
86
+ files:
87
+ - .gitignore
88
+ - Gemfile
89
+ - README.md
90
+ - Rakefile
91
+ - ahora.gemspec
92
+ - lib/ahora.rb
93
+ - lib/ahora/middleware.rb
94
+ - lib/ahora/middleware/last_modified_caching.rb
95
+ - lib/ahora/middleware/request_logger.rb
96
+ - lib/ahora/representation.rb
97
+ - lib/ahora/resource.rb
98
+ - lib/ahora/version.rb
99
+ - test/ahora/representation_test.rb
100
+ - test/ahora_test.rb
101
+ - test/fixtures/employee.xml
102
+ - test/fixtures/user_posts.xml
103
+ - test/test_helper.rb
104
+ homepage: ''
105
+ licenses: []
106
+ post_install_message:
107
+ rdoc_options: []
108
+ require_paths:
109
+ - lib
110
+ required_ruby_version: !ruby/object:Gem::Requirement
111
+ none: false
112
+ requirements:
113
+ - - ! '>='
114
+ - !ruby/object:Gem::Version
115
+ version: '0'
116
+ required_rubygems_version: !ruby/object:Gem::Requirement
117
+ none: false
118
+ requirements:
119
+ - - ! '>='
120
+ - !ruby/object:Gem::Version
121
+ version: '0'
122
+ requirements: []
123
+ rubyforge_project:
124
+ rubygems_version: 1.8.15
125
+ signing_key:
126
+ specification_version: 3
127
+ summary: Consume Java-ish XML HTTP Resources easily
128
+ test_files: []