ahora 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []