acfs 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/.gitignore +0 -1
  2. data/.travis.yml +1 -3
  3. data/{LICENSE.txt → LICENSE} +0 -0
  4. data/README.md +45 -7
  5. data/acfs.gemspec +3 -0
  6. data/gemfiles/{Gemfile.rails-3-1 → Gemfile.rails-4-0} +2 -2
  7. data/lib/acfs.rb +49 -5
  8. data/lib/acfs/adapter/typhoeus.rb +42 -0
  9. data/lib/acfs/collection.rb +22 -0
  10. data/lib/acfs/middleware/base.rb +21 -0
  11. data/lib/acfs/middleware/json_decoder.rb +15 -0
  12. data/lib/acfs/middleware/print.rb +23 -0
  13. data/lib/acfs/model.rb +25 -12
  14. data/lib/acfs/{attributes.rb → model/attributes.rb} +36 -40
  15. data/lib/acfs/model/attributes/integer.rb +18 -0
  16. data/lib/acfs/model/attributes/string.rb +18 -0
  17. data/lib/acfs/{initialization.rb → model/initialization.rb} +4 -1
  18. data/lib/acfs/model/loadable.rb +18 -0
  19. data/lib/acfs/model/locatable.rb +24 -0
  20. data/lib/acfs/model/query_methods.rb +66 -0
  21. data/lib/acfs/model/relations.rb +10 -0
  22. data/lib/acfs/model/service.rb +29 -0
  23. data/lib/acfs/request.rb +33 -0
  24. data/lib/acfs/request/callbacks.rb +46 -0
  25. data/lib/acfs/response.rb +21 -0
  26. data/lib/acfs/response/formats.rb +12 -0
  27. data/lib/acfs/service.rb +30 -0
  28. data/lib/acfs/version.rb +1 -1
  29. data/spec/acfs/middleware/json_decoder_spec.rb +45 -0
  30. data/spec/acfs/request/callbacks_spec.rb +34 -0
  31. data/spec/acfs/request_spec.rb +71 -0
  32. data/spec/acfs_spec.rb +69 -0
  33. data/spec/{attributes_spec.rb → model/attributes_spec.rb} +25 -12
  34. data/spec/model/initialization_spec.rb +30 -0
  35. data/spec/model/locatable_spec.rb +15 -0
  36. data/spec/service_spec.rb +37 -0
  37. data/spec/spec_helper.rb +2 -0
  38. data/spec/support/service.rb +24 -0
  39. metadata +89 -23
  40. data/lib/acfs/attributes/integer.rb +0 -15
  41. data/lib/acfs/attributes/string.rb +0 -15
  42. data/lib/acfs/client.rb +0 -15
  43. data/spec/client_spec.rb +0 -12
  44. data/spec/initialization_spec.rb +0 -22
  45. data/spec/support/my_client.rb +0 -12
@@ -0,0 +1,18 @@
1
+ module Acfs::Model
2
+ module Attributes
3
+
4
+ # Integer attribute type. Use it in your model as an attribute type:
5
+ #
6
+ # class User
7
+ # include Acfs::Model
8
+ # attribute :name, :integer
9
+ # end
10
+ #
11
+ module Integer # :nodoc:
12
+
13
+ def self.cast(obj)
14
+ obj.to_i
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ module Acfs::Model
2
+ module Attributes
3
+
4
+ # String attribute type. Use it in your model as an attribute type:
5
+ #
6
+ # class User
7
+ # include Acfs::Model
8
+ # attribute :name, :string
9
+ # end
10
+ #
11
+ module String # :nodoc:
12
+
13
+ def self.cast(obj)
14
+ obj.to_s
15
+ end
16
+ end
17
+ end
18
+ end
@@ -1,4 +1,7 @@
1
- module Acfs
1
+ module Acfs::Model
2
+
3
+ # Initialization drop-in for pre-4.0 ActiveModel.
4
+ #
2
5
  module Initialization
3
6
 
4
7
  # Initializes a new model with the given +params+.
@@ -0,0 +1,18 @@
1
+ module Acfs::Model
2
+
3
+ module Loadable
4
+ extend ActiveSupport::Concern
5
+
6
+ # Check if model is loaded or if request is still queued.
7
+ #
8
+ def loaded?
9
+ !!@loaded
10
+ end
11
+
12
+ # Mark model as loaded.
13
+ #
14
+ def loaded!
15
+ @loaded = true
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,24 @@
1
+ module Acfs::Model
2
+
3
+ # Provide methods for generation URLs for resources.
4
+ #
5
+ # Example
6
+ # class User
7
+ # service AccountService # With base URL `http://acc.svr`
8
+ # end
9
+ # User.url #=> "http://acc.svr/users"
10
+ # User.url(5) #=> "http://acc.svr/users/5"
11
+ #
12
+ module Locatable
13
+ extend ActiveSupport::Concern
14
+
15
+ module ClassMethods
16
+
17
+ # Return URL for this resource.
18
+ #
19
+ def url(suffix = nil)
20
+ service.url_for(self, suffix: suffix)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,66 @@
1
+ module Acfs::Model
2
+
3
+ # Methods providing the query interface for finding resouces.
4
+ #
5
+ # Example
6
+ # class MyUser
7
+ # include Acfs::Model
8
+ # end
9
+ #
10
+ # MyUser.find(5) # Find single resource
11
+ # MyUser.all # Full or partial collection of
12
+ # # resources
13
+ # Comment.where(user: user.id) # Collection with additional parameter
14
+ # # to filter resources
15
+ #
16
+ module QueryMethods
17
+ extend ActiveSupport::Concern
18
+
19
+ module ClassMethods
20
+
21
+ # Try to load a resource by given id.
22
+ #
23
+ # Example
24
+ # User.find(5) # Will query `http://base.url/users/5`
25
+ #
26
+ def find(id, options = {}, &block)
27
+ model = self.new
28
+
29
+ request = case id
30
+ when Hash
31
+ Acfs::Request.new url, params: id
32
+ else
33
+ Acfs::Request.new url(id.to_s)
34
+ end
35
+
36
+ service.queue(request) do |response|
37
+ model.attributes = response.data
38
+ model.loaded!
39
+ block.call model unless block.nil?
40
+ end
41
+
42
+ model
43
+ end
44
+
45
+ # Try to load all resources.
46
+ #
47
+ def all(params = {}, &block)
48
+ collection = ::Acfs::Collection.new
49
+
50
+ service.queue(Acfs::Request.new(url, params: params)) do |response|
51
+ response.data.each do |obj|
52
+ collection << self.new.tap do |m|
53
+ m.attributes = obj
54
+ m.loaded!
55
+ end
56
+ end
57
+ collection.loaded!
58
+ block.call collection unless block.nil?
59
+ end
60
+
61
+ collection
62
+ end
63
+ alias :where :all
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,10 @@
1
+ module Acfs::Model
2
+
3
+ module Relations
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,29 @@
1
+ module Acfs::Model
2
+
3
+ # Included by Acfs::Model. Allows a model to belong to a service.
4
+ #
5
+ module Service
6
+ extend ActiveSupport::Concern
7
+
8
+ module ClassMethods
9
+
10
+ # Link to service this model belongs to. Connection settings like base URL
11
+ # are fetched from service. Return assigned service if no arguments are given.
12
+ #
13
+ # Example
14
+ # class AccountService < Acfs::Client
15
+ # self.base_url = 'http://acc.serv.org'
16
+ # end
17
+ #
18
+ # class MyUser
19
+ # service AccountService
20
+ # end
21
+ # MyUser.find 5 # Will fetch `http://acc.serv.org/users/5`
22
+ #
23
+ def service(klass = nil, options = {})
24
+ return @service unless klass
25
+ @service = klass.new options
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,33 @@
1
+ require 'acfs/request/callbacks'
2
+
3
+ module Acfs
4
+
5
+ class Request
6
+ attr_accessor :body, :format
7
+ attr_reader :url, :headers, :params, :data
8
+
9
+ include Request::Callbacks
10
+
11
+ def initialize(url, options = {})
12
+ @url = URI.parse(url).tap do |url|
13
+ @data = options.delete(:data) || nil
14
+ @format = options.delete(:format) || :json
15
+ @headers = options.delete(:headers) || {}
16
+ @params = options.delete(:params) || {}
17
+
18
+ url.query = nil # params.any? ? params.to_param : nil
19
+ end.to_s
20
+ end
21
+
22
+ def data?
23
+ !data.nil?
24
+ end
25
+
26
+ class << self
27
+ def new(*attrs)
28
+ return attrs[0] if attrs[0].is_a? self
29
+ super
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,46 @@
1
+ module Acfs
2
+ class Request
3
+
4
+ # Module containing callback handling for Requests.
5
+ # Current the only callback type is `on_complete`:
6
+ #
7
+ # request = Request.new 'URL'
8
+ # request.on_complete { |response| ... }
9
+ #
10
+ module Callbacks
11
+
12
+ # Add a new `on_complete` callback for this request.
13
+ #
14
+ # @example Set on_complete.
15
+ # request.on_complete { |response| print response.body }
16
+ #
17
+ # @param [ Block ] block The callback block to execute.
18
+ #
19
+ # @yield [ Acfs::Response ]
20
+ #
21
+ # @return [ Acfs::Request ] The request itself.
22
+ #
23
+ def on_complete(&block)
24
+ callbacks << block if block_given?
25
+ self
26
+ end
27
+
28
+ # Return array of all callbacks.
29
+ #
30
+ # @return [ Array<Block> ] All callbacks.
31
+ #
32
+ def callbacks
33
+ @callbacks ||= []
34
+ end
35
+
36
+ # Trigger all callback for given response.
37
+ #
38
+ # @return [ Acfs::Request ] The request itself.
39
+ #
40
+ def complete!(response)
41
+ callbacks.each { |cb| cb.call response }
42
+ self
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,21 @@
1
+ require 'acfs/response/formats'
2
+
3
+ module Acfs
4
+
5
+ # This represents a response. In addition to an standard HTTP
6
+ # it has a field `data` for storing the encoded body.
7
+ #
8
+ class Response
9
+ attr_accessor :data
10
+ attr_reader :request, :status, :headers, :body
11
+
12
+ include Response::Formats
13
+
14
+ def initialize(request, status = 200, headers = {}, body = nil)
15
+ @request = request
16
+ @status = status
17
+ @headers = headers
18
+ @body = body
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,12 @@
1
+ module Acfs
2
+ class Response
3
+
4
+ # Quick accessors for format handling.
5
+ module Formats
6
+
7
+ def json?
8
+ headers['Content-Type'] == 'application/json'
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,30 @@
1
+ module Acfs
2
+
3
+ # Service object.
4
+ #
5
+ class Service
6
+ attr_accessor :options
7
+ class_attribute :base_url
8
+
9
+ def initialize(options = {})
10
+ @options = options
11
+ end
12
+
13
+ def options
14
+ @options
15
+ end
16
+
17
+ def url_for(resource_class, options = {})
18
+ options.reverse_merge! self.options
19
+
20
+ url = self.class.base_url.to_s
21
+ url += "/#{(options[:path] || resource_class.name.pluralize.underscore).to_s}"
22
+ url += "/#{options[:suffix].to_s}" if options[:suffix]
23
+ url
24
+ end
25
+
26
+ def queue(request, &block)
27
+ Acfs.queue request, &block
28
+ end
29
+ end
30
+ end
data/lib/acfs/version.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  module Acfs
2
2
  module VERSION
3
3
  MAJOR = 0
4
- MINOR = 2
4
+ MINOR = 3
5
5
  PATCH = 0
6
6
  STAGE = nil
7
7
 
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+
3
+ describe Acfs::Middleware::JsonDecoder do
4
+ let(:data) { [{id: 1, name: "Anon"},{id: 2, name:"John", friends: [ 1 ]}] }
5
+ let(:body) { data.to_param }
6
+ let(:headers) { {} }
7
+ let(:request) { Acfs::Request.new "fubar" }
8
+ let(:response) { Acfs::Response.new request, 200, headers, body }
9
+ let(:decoder) { Acfs::Middleware::JsonDecoder.new lambda { |req| req } }
10
+
11
+ before do
12
+ decoder.call request
13
+ end
14
+
15
+ context 'with JSON response' do
16
+ let(:headers) { { 'Content-Type' => 'application/json' } }
17
+ let(:body) { data.to_json }
18
+
19
+ it 'should decode body data' do
20
+ request.complete! response
21
+
22
+ expect(response.data).to be == data.map(&:stringify_keys)
23
+ end
24
+ end
25
+
26
+ context 'with invalid JSON response' do
27
+ let(:headers) { { 'Content-Type' => 'application/json' } }
28
+ let(:body) { data.to_json[4..-4] }
29
+
30
+ it 'should raise an error' do
31
+ expect { request.complete! response }.to raise_error(MultiJson::LoadError)
32
+ end
33
+ end
34
+
35
+ context 'without JSON response' do
36
+ let(:headers) { { 'Content-Type' => 'application/text' } }
37
+ let(:body) { data.to_json }
38
+
39
+ it 'should not decode non-JSON encoded responses' do
40
+ request.complete! response
41
+
42
+ expect(response.data).to be_nil
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,34 @@
1
+ require 'spec_helper'
2
+
3
+ describe Acfs::Request::Callbacks do
4
+ let(:callback) { lambda { |res| } }
5
+ let(:request) { Acfs::Request.new('fubar') }
6
+
7
+ describe '#on_complete' do
8
+ it 'should store a given callback' do
9
+ request.on_complete &callback
10
+
11
+ expect(request.callbacks).to have(1).item
12
+ expect(request.callbacks[0]).to be == callback
13
+ end
14
+
15
+ it 'should store multiple callback' do
16
+ request.on_complete { |res| "abc" }
17
+ request.on_complete &callback
18
+
19
+ expect(request.callbacks).to have(2).item
20
+ expect(request.callbacks[1]).to be == callback
21
+ end
22
+ end
23
+
24
+ describe '#complete!' do
25
+ let(:response) { Acfs::Response.new(request) }
26
+
27
+ it 'should trigger registered callbacks with given response' do
28
+ callback.should_receive(:call).with(response)
29
+
30
+ request.on_complete &callback
31
+ request.complete! response
32
+ end
33
+ end
34
+ end