acfs 0.7.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 07e4fa8c5a501299db87e1fd63da042f42b4064d
4
- data.tar.gz: 267732ea7fae5e6d110038ff8769ceb8ee3fb9dd
3
+ metadata.gz: 70e3e163082aec963c5162cd4fd59d69eb915819
4
+ data.tar.gz: c4385962f0d06f27a89cb48e4d5d214e904478a9
5
5
  SHA512:
6
- metadata.gz: 1ef40e8272f8396f232e14efd645ded71b393cdeeea7ddd4057b3e671d212ab627d03e39b58e32e62f90e478b26656b1856ad48e9ad8cdf747c707ae284d5001
7
- data.tar.gz: d5693941256b767e34fe7934bdfba838156af64f00d1c3d7bd29fc24a1483b4473d3adcfeba616d709183c14bbbe7db8790c8864a3d8918eb2db2b74ec3ea58d
6
+ metadata.gz: f1e2908198cf1314b37fa8c1829ac1995ec36458069203daf334ff0a46b1e35efe9e022f9b375c12dfe3ef6dff9a140594671e33866ecd479384c8c26a1900a7
7
+ data.tar.gz: d1c128c7028e83f9e79a52bbd449495c38a5aa97c116d02df18828fe2084c739a5b06dd65e805b80e477c19d56e0819c47e802dff01cff45271db2b67e6ccb38
data/README.md CHANGED
@@ -2,15 +2,18 @@
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/acfs.png)](http://badge.fury.io/rb/acfs) [![Build Status](https://travis-ci.org/jgraichen/acfs.png?branch=master)](https://travis-ci.org/jgraichen/acfs) [![Coverage Status](https://coveralls.io/repos/jgraichen/acfs/badge.png?branch=master)](https://coveralls.io/r/jgraichen/acfs) [![Code Climate](https://codeclimate.com/github/jgraichen/acfs.png)](https://codeclimate.com/github/jgraichen/acfs) [![Dependency Status](https://gemnasium.com/jgraichen/acfs.png)](https://gemnasium.com/jgraichen/acfs)
4
4
 
5
- TODO: Develop asynchronous parallel API client library for service oriented applications.
5
+ Acfs is a library to develop API client libraries for single services within a
6
+ larger service oriented application.
6
7
 
7
- TODO: Write a gem description
8
+ Acfs covers model and service abstraction, convenient query and filter methods,
9
+ full middleware stack for pre-processing requests and responses on a per service
10
+ level and automatic request queuing and parallel processing. See Usage for more.
8
11
 
9
12
  ## Installation
10
13
 
11
14
  Add this line to your application's Gemfile:
12
15
 
13
- gem 'acfs', '0.7.0'
16
+ gem 'acfs', '0.8.0'
14
17
 
15
18
  **Note:** Acfs is under development. I'll try to avoid changes to the public
16
19
  API but internal APIs may change quite often.
@@ -31,7 +34,7 @@ First you need to define your service(s):
31
34
  class UserService < Acfs::Service
32
35
  self.base_url = 'http://users.myapp.org'
33
36
 
34
- # You can configure middlewares you want use for the service here.
37
+ # You can configure middlewares you want to use for the service here.
35
38
  # Each service has it own middleware stack.
36
39
  #
37
40
  use Acfs::Middleware::JsonDecoder
@@ -39,8 +42,8 @@ class UserService < Acfs::Service
39
42
  end
40
43
  ```
41
44
 
42
- This specifies where the `UserService` can be reached. You can now create some
43
- models representing resources serviced by the `UserService`.
45
+ This specifies where the `UserService` is located. You can now create some
46
+ models representing resources served by the `UserService`.
44
47
 
45
48
  ```ruby
46
49
  class User
@@ -122,9 +125,30 @@ Acfs.run # This call will fire all request as parallel as possible.
122
125
  @friends[0].name # => "Miraculix"
123
126
  ```
124
127
 
125
- ## TODO
128
+ Acfs has basic update support using `PUT` requests:
126
129
 
127
- * Create/Update operations
130
+ ```ruby
131
+ @user = User.find 5
132
+ @user.name = "Bob"
133
+
134
+ @user.changed? # => true
135
+ @user.persisted? # => false
136
+
137
+ @user.save # Or .save!
138
+ # Will PUT new resource to service synchronously.
139
+
140
+ @user.changed? # => false
141
+ @user.persisted? # => true
142
+ ```
143
+
144
+ ## Roadmap
145
+
146
+ * Create operations
147
+ * Update
148
+ * Better new? detection eg. storing ETag from request resources.
149
+ * Use PATCH for with only changed attributes and `If-Unmodifed-Since`
150
+ and `If-Match` header fields if resource was surly loaded from service
151
+ and not created with an id (e.g `User.new id: 5, name: "john"`).
128
152
  * High level features
129
153
  * Pagination? Filtering? (If service API provides such features.)
130
154
  * Messaging Queue support for services and models
data/lib/acfs.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require 'active_support'
2
2
  require 'active_support/core_ext'
3
3
  require 'acfs/version'
4
+ require 'acfs/errors'
4
5
 
5
6
  module Acfs
6
7
  extend ActiveSupport::Autoload
@@ -18,6 +19,8 @@ module Acfs
18
19
  autoload :Print
19
20
  autoload :JsonDecoder
20
21
  autoload :MessagePackDecoder, 'acfs/middleware/msgpack_decoder'
22
+ autoload :JsonEncoder
23
+ autoload :MessagePackEncoder, 'acfs/middleware/msgpack_encoder'
21
24
  end
22
25
 
23
26
  module Adapter
@@ -9,8 +9,10 @@ module Acfs
9
9
 
10
10
  # Run all queued requests.
11
11
  #
12
- def run
13
- hydra.run
12
+ def run(request = nil)
13
+ return hydra.run unless request
14
+
15
+ convert_request(request).run
14
16
  end
15
17
 
16
18
  # Add a new request or URL to the queue.
@@ -25,7 +27,11 @@ module Acfs
25
27
  end
26
28
 
27
29
  def convert_request(req)
28
- request = ::Typhoeus::Request.new req.url, params: req.params, headers: req.headers, body: req.body
30
+ request = ::Typhoeus::Request.new req.url,
31
+ method: req.method,
32
+ params: req.params,
33
+ headers: req.headers,
34
+ body: req.body
29
35
 
30
36
  request.on_complete do |response|
31
37
  req.complete! convert_response(req, response)
@@ -0,0 +1,7 @@
1
+ module Acfs
2
+
3
+ # Acfs base error.
4
+ #
5
+ class Error < StandardError; end
6
+
7
+ end
@@ -0,0 +1,20 @@
1
+ require 'multi_json'
2
+
3
+ module Acfs
4
+ module Middleware
5
+
6
+ # A middleware to encore request data using JSON.
7
+ #
8
+ class JsonEncoder < Base
9
+
10
+ def call(request)
11
+ unless request.method == :get or request.data.nil?
12
+ request.body = ::MultiJson.dump(request.data)
13
+ request.headers['Content-Type'] = 'application/json'
14
+ end
15
+
16
+ app.call request
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,19 @@
1
+ require 'msgpack'
2
+ require 'action_dispatch'
3
+
4
+ module Acfs
5
+ module Middleware
6
+
7
+ # A middleware to encode request data with Message Pack.
8
+ #
9
+ class MessagePackEncoder < Base
10
+
11
+ def call(request)
12
+ request.body = ::MessagePack.dump(request.data)
13
+ request.headers['Content-Type'] = 'application/x-msgpack'
14
+
15
+ app.call request
16
+ end
17
+ end
18
+ end
19
+ end
data/lib/acfs/model.rb CHANGED
@@ -1,8 +1,10 @@
1
1
  require 'active_model'
2
2
 
3
3
  require 'acfs/model/attributes'
4
+ require 'acfs/model/dirty'
4
5
  require 'acfs/model/loadable'
5
6
  require 'acfs/model/locatable'
7
+ require 'acfs/model/persistence'
6
8
  require 'acfs/model/query_methods'
7
9
  require 'acfs/model/relations'
8
10
  require 'acfs/model/service'
@@ -26,10 +28,12 @@ module Acfs
26
28
 
27
29
  include Model::Attributes
28
30
  include Model::Loadable
31
+ include Model::Persistence
29
32
  include Model::Locatable
30
33
  include Model::QueryMethods
31
34
  include Model::Relations
32
35
  include Model::Service
36
+ include Model::Dirty
33
37
  end
34
38
  end
35
39
  end
@@ -16,10 +16,11 @@ module Acfs::Model
16
16
  #
17
17
  module Attributes
18
18
  extend ActiveSupport::Concern
19
+ include ActiveModel::AttributeMethods
19
20
 
20
21
  def initialize(*attrs) # :nodoc:
21
- self.class.attributes.each { |k, v| public_send :"#{k}=", v }
22
- super *attrs
22
+ self.write_attributes self.class.attributes, change: false
23
+ super
23
24
  end
24
25
 
25
26
  # Returns ActiveModel compatible list of attributes and values.
@@ -38,11 +39,50 @@ module Acfs::Model
38
39
  # Update all attributes with given hash.
39
40
  #
40
41
  def attributes=(attributes)
41
- self.attributes.each do |key, _|
42
- send :"#{key}=", attributes[key] if attributes.has_key? key
42
+ write_attributes attributes
43
+ end
44
+
45
+ # Read an attribute.
46
+ #
47
+ def read_attribute(name)
48
+ instance_variable_get :"@#{name}"
49
+ end
50
+
51
+ # Write a hash of attributes and values.
52
+ #
53
+ def write_attributes(attributes, opts = {})
54
+ procs = {}
55
+
56
+ attributes.each do |key, _|
57
+ if attributes[key].is_a? Proc
58
+ procs[key] = attributes[key]
59
+ else
60
+ write_attribute key, attributes[key], opts
61
+ end
62
+ end
63
+
64
+ procs.each do |key, proc|
65
+ write_attribute key, instance_exec(&proc), opts
43
66
  end
44
67
  end
45
68
 
69
+ # Write an attribute.
70
+ #
71
+ def write_attribute(name, value, opts = {})
72
+ if (type = self.class.attribute_types[name.to_sym]).nil?
73
+ raise "Unknown attribute #{name}."
74
+ end
75
+
76
+ write_raw_attribute name, value.nil? ? nil : type.cast(value), opts
77
+ end
78
+
79
+ # Write an attribute without checking type and existence or casting
80
+ # value to attributes type.
81
+ #
82
+ def write_raw_attribute(name, value, _ = {})
83
+ instance_variable_set :"@#{name}", value
84
+ end
85
+
46
86
  module ClassMethods # :nodoc:
47
87
 
48
88
  # Define a model attribute by name and type. Will create getter and
@@ -76,17 +116,26 @@ module Acfs::Model
76
116
  @attributes ||= {}
77
117
  end
78
118
 
119
+ # Return hash of attributes and there types.
120
+ #
121
+ def attribute_types
122
+ @attribute_types ||= {}
123
+ end
124
+
79
125
  private
80
126
  def define_attribute(name, type, opts = {}) # :nodoc:
81
- @attributes ||= {}
82
- @attributes[name] = type.cast opts.has_key?(:default) ? opts[:default] : nil
127
+ default_value = opts.has_key?(:default) ? opts[:default] : nil
128
+ default_value = type.cast default_value unless default_value.is_a? Proc or default_value.nil?
129
+ attributes[name] = default_value
130
+ attribute_types[name.to_sym] = type
131
+ define_attribute_method name
83
132
 
84
133
  self.send :define_method, name do
85
- instance_variable_get :"@#{name}"
134
+ read_attribute name
86
135
  end
87
136
 
88
137
  self.send :define_method, :"#{name}=" do |value|
89
- instance_variable_set :"@#{name}", type.cast(value)
138
+ write_attribute name, value
90
139
  end
91
140
  end
92
141
  end
@@ -0,0 +1,39 @@
1
+ module Acfs
2
+ module Model
3
+
4
+ # Thin wrapper around ActiveModel::Dirty
5
+ #
6
+ module Dirty
7
+ extend ActiveSupport::Concern
8
+ include ActiveModel::Dirty
9
+
10
+ # Resets all changes. Do not touch previous changes.
11
+ #
12
+ def reset_changes
13
+ changed_attributes.clear
14
+ end
15
+
16
+ # Save current changes as previous changes and reset
17
+ # current one.
18
+ #
19
+ def swap_changes
20
+ @previously_changed = changes
21
+ reset_changes
22
+ end
23
+
24
+ def save!(*) # :nodoc:
25
+ super.tap { |__| swap_changes }
26
+ end
27
+
28
+ def loaded! # :nodoc:
29
+ reset_changes
30
+ super
31
+ end
32
+
33
+ def write_raw_attribute(name, value, opts = {}) # :nodoc:
34
+ attribute_will_change! name if opts[:change].nil? or opts[:change]
35
+ super
36
+ end
37
+ end
38
+ end
39
+ end
@@ -24,18 +24,5 @@ module Acfs::Model
24
24
  end if params
25
25
  end
26
26
 
27
- # Indicates if the model is persisted. Default is +false+.
28
- #
29
- # class User
30
- # include Acfs::Model
31
- # attribute :name
32
- # end
33
- #
34
- # user = User.new(name: 'bob')
35
- # user.persisted? # => false
36
- #
37
- def persisted?
38
- false
39
- end
40
27
  end
41
28
  end
@@ -20,5 +20,10 @@ module Acfs::Model
20
20
  service.url_for(self, suffix: suffix)
21
21
  end
22
22
  end
23
+
24
+ def url
25
+ return nil if id.nil?
26
+ self.class.service.url_for self, suffix: read_attribute(:id)
27
+ end
23
28
  end
24
29
  end
@@ -0,0 +1,73 @@
1
+ module Acfs
2
+ module Model
3
+
4
+ # Allow to track the persistence state of a model.
5
+ #
6
+ module Persistence
7
+
8
+ # Check if the model is persisted. A model is persisted if
9
+ # it is saved after beeing created or when it was not changed
10
+ # since it was loaded.
11
+ #
12
+ # user = User.new name: "John"
13
+ # user.persisted? # => false
14
+ # user.save
15
+ # user.persisted? # => true
16
+ #
17
+ # user2 = User.find 5
18
+ # user2.persisted? # => true
19
+ # user2.name = 'Amy'
20
+ # user2.persisted? # => false
21
+ # user2.save
22
+ # user2.persisted? # => true
23
+ #
24
+ def persisted?
25
+ !new? && !changed?
26
+ end
27
+
28
+ # Return true if model is a new record and was not saved yet.
29
+ #
30
+ def new?
31
+ read_attribute(:id).nil?
32
+ end
33
+ alias :new_record? :new?
34
+
35
+ # Save the resource.
36
+ #
37
+ # It will PATCH to the service to update the resource or send
38
+ # a POST to create a new one if the resource is new.
39
+ #
40
+ # `#save` return true of operation was successful, otherwise false.
41
+ #
42
+ def save(*args)
43
+ save! *args
44
+ true
45
+ rescue Acfs::Error
46
+ false
47
+ end
48
+
49
+ def save!(*) # :nodoc:
50
+ request = new? ? create_request : put_request
51
+ request.on_complete do |response|
52
+ update_with response.data
53
+ end
54
+
55
+ self.class.service.run request
56
+ end
57
+
58
+ private
59
+ def update_with(data)
60
+ self.attributes = data
61
+ loaded!
62
+ end
63
+
64
+ def create_request
65
+ Acfs::Request.new self.class.url, method: :post, data: attributes
66
+ end
67
+
68
+ def put_request
69
+ Acfs::Request.new url, method: :put, data: attributes
70
+ end
71
+ end
72
+ end
73
+ end
data/lib/acfs/request.rb CHANGED
@@ -7,7 +7,7 @@ module Acfs
7
7
  #
8
8
  class Request
9
9
  attr_accessor :body, :format
10
- attr_reader :url, :headers, :params, :data
10
+ attr_reader :url, :headers, :params, :data, :method
11
11
 
12
12
  include Request::Callbacks
13
13
 
@@ -17,6 +17,7 @@ module Acfs
17
17
  @format = options.delete(:format) || :json
18
18
  @headers = options.delete(:headers) || {}
19
19
  @params = options.delete(:params) || {}
20
+ @method = options.delete(:method) || :get
20
21
 
21
22
  url.query = nil # params.any? ? params.to_param : nil
22
23
  end.to_s
@@ -7,15 +7,20 @@ module Acfs
7
7
  module Formats
8
8
 
9
9
  def content_type
10
- @content_type ||= begin
11
- content_type = headers['Content-Type'].split(/;\s*\w+="?\w+"?/).first
12
- Mime::Type.parse(content_type).first
13
- end
10
+ @content_type ||= read_content_type
14
11
  end
15
12
 
16
13
  def json?
17
14
  content_type == Mime::JSON
18
15
  end
16
+
17
+ private
18
+ def read_content_type
19
+ return 'text/plain' unless headers && headers['Content-Type']
20
+
21
+ content_type = headers['Content-Type'].split(/;\s*\w+="?\w+"?/).first
22
+ Mime::Type.parse(content_type).first
23
+ end
19
24
  end
20
25
  end
21
26
  end
data/lib/acfs/service.rb CHANGED
@@ -1,5 +1,5 @@
1
+ require 'acfs/service/request_handler'
1
2
  require 'acfs/service/middleware'
2
- require 'acfs/service/queue'
3
3
 
4
4
  module Acfs
5
5
 
@@ -9,7 +9,7 @@ module Acfs
9
9
  attr_accessor :options
10
10
  class_attribute :base_url
11
11
 
12
- include Service::Queue
12
+ include Service::RequestHandler
13
13
  include Service::Middleware
14
14
 
15
15
  def initialize(options = {})
@@ -8,11 +8,8 @@ module Acfs
8
8
  module Middleware
9
9
  extend ActiveSupport::Concern
10
10
 
11
- # Queue a new request. The request will travel through
12
- # all registered middleware.
13
- #
14
- def queue(req)
15
- super self.class.middleware.call req
11
+ def prepare(_) # :nodoc:
12
+ self.class.middleware.call super
16
13
  end
17
14
 
18
15
  module ClassMethods
@@ -0,0 +1,27 @@
1
+ module Acfs
2
+ class Service
3
+
4
+ # Methods to queue or executed request through this service.
5
+ #
6
+ module RequestHandler
7
+
8
+ # Queue a new request on global adapter queue.
9
+ #
10
+ def queue(request)
11
+ Acfs.adapter.queue prepare request
12
+ end
13
+
14
+ # Executes a request now.
15
+ #
16
+ def run(request)
17
+ Acfs.adapter.run prepare request
18
+ end
19
+
20
+ # Prepares a request to be processed through this service.
21
+ #
22
+ def prepare(request)
23
+ request
24
+ end
25
+ end
26
+ end
27
+ 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 = 7
4
+ MINOR = 8
5
5
  PATCH = 0
6
6
  STAGE = nil
7
7
 
@@ -13,6 +13,17 @@ describe Acfs::Model::Attributes do
13
13
  it 'should set default attributes' do
14
14
  expect(model.new.name).to be == 'John'
15
15
  end
16
+
17
+ context 'with dynamic default value' do
18
+ before do
19
+ model.attribute :name, :string, default: 'John'
20
+ model.attribute :mail, :string, default: -> { "#{name}@srv.tld" }
21
+ end
22
+
23
+ it 'should set dynamic default attributes' do
24
+ expect(model.new.mail).to be == 'John@srv.tld'
25
+ end
26
+ end
16
27
  end
17
28
 
18
29
  describe '#attributes' do
@@ -90,7 +101,7 @@ describe Acfs::Model::Attributes do
90
101
  it 'should add an attribute to model attribute list' do
91
102
  model.send :attribute, :name, :string
92
103
 
93
- expect(model.attributes).to be == { :name => '' }
104
+ expect(model.attributes).to be == { :name => nil }
94
105
  end
95
106
 
96
107
  it 'should accept a default value' do
@@ -0,0 +1,50 @@
1
+ require 'spec_helper'
2
+
3
+ describe Acfs::Model::Dirty do
4
+ let(:model) { MyUser.new }
5
+ before do
6
+ stub_request(:get, "http://users.example.org/users/1").to_return(
7
+ body: MessagePack.dump({ id: 1, name: 'Anon', age: 12 }),
8
+ headers: {'Content-Type' => 'application/x-msgpack'})
9
+
10
+ stub_request(:post, 'http://users.example.org/users').to_return(
11
+ body: MessagePack.dump({ id: 5, name: 'dhh', age: 12 }),
12
+ headers: {'Content-Type' => 'application/x-msgpack'})
13
+ end
14
+
15
+ it 'includes ActiveModel::Dirty' do
16
+ model.is_a? ActiveModel::Dirty
17
+ end
18
+
19
+ describe '#changed?' do
20
+ context 'after attribute change' do
21
+ let(:user) { MyUser.new name: 'dhh' }
22
+
23
+ it { expect(user).to be_changed }
24
+
25
+ context 'and saving' do
26
+ before { user.save }
27
+ it { expect(user).to_not be_changed }
28
+ end
29
+ end
30
+
31
+ context 'after model load' do
32
+ let(:user) { MyUser.find 1 }
33
+ before { user; Acfs.run}
34
+
35
+ it { expect(user).to_not be_changed }
36
+ end
37
+
38
+ context 'after model new without attrs' do
39
+ let(:user) { MyUser.new }
40
+
41
+ it { expect(user).to_not be_changed }
42
+ end
43
+
44
+ context 'after model new with attrs' do
45
+ let(:user) { MyUser.new name: "Uschi" }
46
+
47
+ it { expect(user).to be_changed }
48
+ end
49
+ end
50
+ end
File without changes
@@ -0,0 +1,21 @@
1
+ require 'spec_helper'
2
+
3
+ describe Acfs::Model::Loadable do
4
+ let(:model) { MyUser.find 1 }
5
+ before do
6
+ stub_request(:get, "http://users.example.org/users/1").to_return(
7
+ body: MessagePack.dump({ id: 1, name: "Anon", age: 12 }),
8
+ headers: {'Content-Type' => 'application/x-msgpack'})
9
+ end
10
+
11
+ describe '#loaded?' do
12
+ context 'before Acfs#run' do
13
+ it { expect(model).to_not be_loaded }
14
+ end
15
+
16
+ context 'afer Acfs#run' do
17
+ before { model; Acfs.run}
18
+ it { expect(model).to be_loaded }
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,47 @@
1
+ require 'spec_helper'
2
+
3
+ describe Acfs::Model::Locatable do
4
+ let(:model) { MyUser }
5
+ before do
6
+ stub_request(:get, "http://users.example.org/users/1").to_return(
7
+ body: MessagePack.dump({ id: 1, name: "Anon", age: 12 }),
8
+ headers: {'Content-Type' => 'application/x-msgpack'})
9
+ end
10
+
11
+ describe '.url' do
12
+ it 'should return URL' do
13
+ expect(model.url).to be == 'http://users.example.org/users'
14
+ end
15
+
16
+ it 'should return URL with id path part if specified' do
17
+ expect(model.url(5)).to be == 'http://users.example.org/users/5'
18
+ end
19
+ end
20
+
21
+ describe '#url' do
22
+ context 'new resource' do
23
+ let(:m) { model.new }
24
+
25
+ it "should return nil" do
26
+ expect(m.url).to be == nil
27
+ end
28
+
29
+ context 'new resource with id' do
30
+ let(:m) { model.new id: 475 }
31
+
32
+ it "should return resource URL" do
33
+ expect(m.url).to be == 'http://users.example.org/users/475'
34
+ end
35
+ end
36
+ end
37
+
38
+ context 'loaded resource' do
39
+ let(:m) { model.find 1 }
40
+ before { m; Acfs.run }
41
+
42
+ it "should return resource's URL" do
43
+ expect(m.url).to be == 'http://users.example.org/users/1'
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,80 @@
1
+ require 'spec_helper'
2
+
3
+ describe Acfs::Model::Persistence do
4
+ let(:model_class) { MyUser }
5
+ before do
6
+ @get_stub = stub_request(:get, "http://users.example.org/users/1").to_return(
7
+ body: MessagePack.dump({ id: 1, name: "Anon", age: 12 }),
8
+ headers: {'Content-Type' => 'application/x-msgpack'})
9
+
10
+ @patch_stub = stub_request(:put, 'http://users.example.org/users/1')
11
+ .with(
12
+ body: '{"id":1,"name":"Idefix","age":12}')
13
+ .to_return(
14
+ body: MessagePack.dump({ id: 1, name: 'Idefix', age: 12 }),
15
+ headers: {'Content-Type' => 'application/x-msgpack'})
16
+
17
+ @post_stub = stub_request(:post, 'http://users.example.org/users').to_return(
18
+ body: MessagePack.dump({ id: 5, name: 'Idefix', age: 12 }),
19
+ headers: {'Content-Type' => 'application/x-msgpack'})
20
+ end
21
+
22
+ context 'new model' do
23
+ let(:model) { model_class.new }
24
+
25
+ it { expect(model).to_not be_persisted }
26
+ it { expect(model).to be_new }
27
+
28
+ describe '#save!' do
29
+ context 'when modified' do
30
+ let(:model) { model_class.find 1 }
31
+ before do
32
+ model
33
+ Acfs.run
34
+ model.name = 'Idefix'
35
+ end
36
+
37
+ it 'should PUT to model URL' do
38
+ model.save!
39
+
40
+ expect(@patch_stub).to have_been_requested
41
+ end
42
+ end
43
+
44
+ context 'when new' do
45
+ let(:model) { model_class.new name: 'Idefix', age: 12 }
46
+
47
+ it 'should POST to collection URL' do
48
+ model.save!
49
+
50
+ expect(@post_stub).to have_been_requested
51
+ end
52
+ end
53
+ end
54
+
55
+ context 'after save' do
56
+ before { model.save! }
57
+
58
+ it { expect(model).to be_persisted }
59
+ it { expect(model).to_not be_new }
60
+ end
61
+ end
62
+
63
+ context 'loaded model' do
64
+ context 'without changes' do
65
+ let(:model) { model_class.find 1 }
66
+ before { model; Acfs.run }
67
+
68
+ it { expect(model).to be_persisted }
69
+ it { expect(model).to_not be_new }
70
+ end
71
+
72
+ context 'with changes' do
73
+ let(:model) { model_class.find 1 }
74
+ before { model; Acfs.run; model.name = "dhh" }
75
+
76
+ it { expect(model).to_not be_persisted }
77
+ it { expect(model).to_not be_new }
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,65 @@
1
+ require 'spec_helper'
2
+
3
+ describe Acfs::Model::QueryMethods do
4
+ let(:model) { MyUser }
5
+
6
+ describe '.find' do
7
+ context 'with single id' do
8
+ before do
9
+ stub_request(:get, 'http://users.example.org/users/1').to_return(
10
+ body: MessagePack.dump({ id: 1, name: 'Anon', age: 12 }),
11
+ headers: {'Content-Type' => 'application/x-msgpack'})
12
+ end
13
+
14
+ it 'should load a single remote resource' do
15
+ user = model.find 1
16
+ Acfs.run
17
+
18
+ expect(user.attributes).to be == { id: 1, name: 'Anon', age: 12 }.stringify_keys
19
+ end
20
+
21
+ it 'should invoke callback after model is loaded' do
22
+ proc = Proc.new { }
23
+ proc.should_receive(:call) do |user|
24
+ expect(user).to be === @user
25
+ expect(user).to be_loaded
26
+ end
27
+
28
+ @user = model.find 1, &proc
29
+ Acfs.run
30
+ end
31
+ end
32
+
33
+ context 'with multiple ids' do
34
+ before do
35
+ stub_request(:get, 'http://users.example.org/users/1').to_return(
36
+ body: MessagePack.dump({ id: 1, name: 'Anon', age: 12 }),
37
+ headers: {'Content-Type' => 'application/x-msgpack'})
38
+ stub_request(:get, 'http://users.example.org/users/2').to_return(
39
+ body: MessagePack.dump({ id: 2, name: 'Johnny', age: 42 }),
40
+ headers: {'Content-Type' => 'application/x-msgpack'})
41
+ end
42
+
43
+ it 'should load a multiple remote resources' do
44
+ users = model.find 1, 2
45
+ Acfs.run
46
+
47
+ expect(users.size).to be == 2
48
+ expect(users[0].attributes).to be == { id: 1, name: 'Anon', age: 12 }.stringify_keys
49
+ expect(users[1].attributes).to be == { id: 2, name: 'Johnny', age: 42 }.stringify_keys
50
+ end
51
+
52
+ it 'should invoke callback after all models are loaded' do
53
+ proc = Proc.new { }
54
+ proc.should_receive(:call) do |users|
55
+ expect(users).to be === @users
56
+ expect(users.size).to be == 2
57
+ expect(users).to be_loaded
58
+ end
59
+
60
+ @users = model.find 1, 2, &proc
61
+ Acfs.run
62
+ end
63
+ end
64
+ end
65
+ end
@@ -5,7 +5,8 @@ describe Acfs::Request do
5
5
  let(:headers) { nil }
6
6
  let(:params) { nil }
7
7
  let(:data) { nil }
8
- let(:options) { {headers: headers, params: params, data: data} }
8
+ let(:method) { :get }
9
+ let(:options) { {method: method, headers: headers, params: params, data: data} }
9
10
  let(:request) { Acfs::Request.new(url, options) }
10
11
 
11
12
  describe '#url' do
@@ -39,6 +40,20 @@ describe Acfs::Request do
39
40
  end
40
41
  end
41
42
 
43
+ describe '#method' do
44
+ context 'when nil given' do
45
+ let(:method) { nil }
46
+
47
+ it 'should default to :get' do
48
+ expect(request.method).to be == :get
49
+ end
50
+ end
51
+
52
+ it 'should return request method' do
53
+ expect(request.method).to be == method
54
+ end
55
+ end
56
+
42
57
  describe '#params' do
43
58
  let(:params) { { id: 10 }}
44
59
 
@@ -8,6 +8,14 @@ describe Acfs::Response::Formats do
8
8
  let(:body) { nil }
9
9
  let(:response) { Acfs::Response.new request, status, headers, body }
10
10
 
11
+ context 'without Content-Type header' do
12
+ let(:headers) { {} }
13
+
14
+ it "should fallback on 'text/plain'" do
15
+ expect(response.content_type).to be == Mime::TEXT
16
+ end
17
+ end
18
+
11
19
  context 'with JSON mimetype' do
12
20
  let(:mime_type) { 'application/json' }
13
21
 
data/spec/acfs_spec.rb CHANGED
@@ -3,18 +3,18 @@ require 'spec_helper'
3
3
  describe "Acfs" do
4
4
 
5
5
  before do
6
- headers = {}
6
+ headers = {}
7
7
  stub_request(:get, "http://users.example.org/users").to_return(
8
8
  body: MessagePack.dump([{ id: 1, name: "Anon", age: 12 }, { id: 2, name: "John", age: 26 }]),
9
9
  headers: headers.merge({'Content-Type' => 'application/x-msgpack'}))
10
10
  stub_request(:get, "http://users.example.org/users/2").to_return(
11
11
  body: MessagePack.dump({ id: 2, name: "John", age: 26 }),
12
- headers: headers.merge({'Content-Type' => 'application/x-msgpack'}))
12
+ headers: headers.merge({'Content-Type' => 'application/x-msgpack', 'ETag' => 'LukeSkywalker' }))
13
13
  stub_request(:get, "http://users.example.org/users/3").to_return(
14
- body: MessagePack.dump({ id: 2, name: "Miraculix", age: 122 }),
14
+ body: MessagePack.dump({ id: 3, name: "Miraculix", age: 122 }),
15
15
  headers: headers.merge({'Content-Type' => 'application/x-msgpack'}))
16
16
  stub_request(:get, "http://users.example.org/users/100").to_return(
17
- body: '{"id":2,"name":"Jimmy","age":45}',
17
+ body: '{"id":100,"name":"Jimmy","age":45}',
18
18
  headers: headers.merge({'Content-Type' => 'application/json'}))
19
19
  stub_request(:get, "http://users.example.org/users/2/friends").to_return(
20
20
  body: '[{"id":1,"name":"Anon","age":12}]',
@@ -24,6 +24,28 @@ describe "Acfs" do
24
24
  headers: headers.merge({'Content-Type' => 'application/json'}))
25
25
  end
26
26
 
27
+ it 'should update single resource synchronously' do
28
+ stub = stub_request(:put, "http://users.example.org/users/2")
29
+ .to_return { |request| { body: request.body, headers: {'Content-Type' => request.headers['Content-Type']}} }
30
+
31
+ @user = MyUser.find(2)
32
+ Acfs.run
33
+
34
+ expect(@user).to_not be_changed
35
+ expect(@user).to be_persisted
36
+
37
+ @user.name = "Johnny"
38
+
39
+ expect(@user).to be_changed
40
+ expect(@user).to_not be_persisted
41
+
42
+ @user.save
43
+
44
+ expect(stub).to have_been_requested
45
+ expect(@user).to_not be_changed
46
+ expect(@user).to be_persisted
47
+ end
48
+
27
49
  it 'should load single resource' do
28
50
  @user = MyUser.find(2)
29
51
 
@@ -32,6 +54,7 @@ describe "Acfs" do
32
54
  Acfs.run
33
55
 
34
56
  expect(@user).to be_loaded
57
+ expect(@user.id).to be == 2
35
58
  expect(@user.name).to be == 'John'
36
59
  expect(@user.age).to be == 26
37
60
  end
@@ -51,14 +74,17 @@ describe "Acfs" do
51
74
  expect(@users).to be_loaded
52
75
  expect(@users).to have(3).items
53
76
 
77
+ expect(@users[0].id).to be == 2
54
78
  expect(@users[0].name).to be == 'John'
55
79
  expect(@users[0].age).to be == 26
56
80
  expect(@users[0]).to be == @john
57
81
 
82
+ expect(@users[1].id).to be == 3
58
83
  expect(@users[1].name).to be == 'Miraculix'
59
84
  expect(@users[1].age).to be == 122
60
85
  expect(@users[1]).to be == @mirx
61
86
 
87
+ expect(@users[2].id).to be == 100
62
88
  expect(@users[2].name).to be == 'Jimmy'
63
89
  expect(@users[2].age).to be == 45
64
90
  expect(@users[2]).to be == @jimy
@@ -95,12 +121,13 @@ describe "Acfs" do
95
121
  end
96
122
 
97
123
  it 'should load associated resources from different service' do
98
- @user = MyUser.find(2) do |user|
124
+ @user = MyUser.find 2 do |user|
99
125
  @comments = Comment.where user: user.id
100
126
  end
101
127
 
102
128
  Acfs.run
103
129
 
130
+ expect(@user.id).to be == 2
104
131
  expect(@user.name).to be == 'John'
105
132
  expect(@user.age).to be == 26
106
133
 
@@ -3,6 +3,7 @@ class UserService < Acfs::Service
3
3
  self.base_url = 'http://users.example.org'
4
4
  use Acfs::Middleware::MessagePackDecoder
5
5
  use Acfs::Middleware::JsonDecoder
6
+ use Acfs::Middleware::JsonEncoder
6
7
  end
7
8
 
8
9
  class CommentService < Acfs::Service
@@ -23,5 +24,6 @@ class Comment
23
24
  include Acfs::Model
24
25
  service CommentService
25
26
 
27
+ attribute :id, :integer
26
28
  attribute :text, :string
27
29
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: acfs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jan Graichen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-04-20 00:00:00.000000000 Z
11
+ date: 2013-04-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -200,18 +200,23 @@ files:
200
200
  - lib/acfs.rb
201
201
  - lib/acfs/adapter/typhoeus.rb
202
202
  - lib/acfs/collection.rb
203
+ - lib/acfs/errors.rb
203
204
  - lib/acfs/middleware/base.rb
204
205
  - lib/acfs/middleware/json_decoder.rb
206
+ - lib/acfs/middleware/json_encoder.rb
205
207
  - lib/acfs/middleware/msgpack_decoder.rb
208
+ - lib/acfs/middleware/msgpack_encoder.rb
206
209
  - lib/acfs/middleware/print.rb
207
210
  - lib/acfs/model.rb
208
211
  - lib/acfs/model/attributes.rb
209
212
  - lib/acfs/model/attributes/boolean.rb
210
213
  - lib/acfs/model/attributes/integer.rb
211
214
  - lib/acfs/model/attributes/string.rb
215
+ - lib/acfs/model/dirty.rb
212
216
  - lib/acfs/model/initialization.rb
213
217
  - lib/acfs/model/loadable.rb
214
218
  - lib/acfs/model/locatable.rb
219
+ - lib/acfs/model/persistence.rb
215
220
  - lib/acfs/model/query_methods.rb
216
221
  - lib/acfs/model/relations.rb
217
222
  - lib/acfs/model/service.rb
@@ -221,18 +226,22 @@ files:
221
226
  - lib/acfs/response/formats.rb
222
227
  - lib/acfs/service.rb
223
228
  - lib/acfs/service/middleware.rb
224
- - lib/acfs/service/queue.rb
229
+ - lib/acfs/service/request_handler.rb
225
230
  - lib/acfs/version.rb
226
231
  - spec/acfs/middleware/json_decoder_spec.rb
227
232
  - spec/acfs/middleware/msgpack_decoder_spec.rb
233
+ - spec/acfs/model/attributes_spec.rb
234
+ - spec/acfs/model/dirty_spec.rb
235
+ - spec/acfs/model/initialization_spec.rb
236
+ - spec/acfs/model/loadable_spec.rb
237
+ - spec/acfs/model/locatable_spec.rb
238
+ - spec/acfs/model/persistance_spec.rb
239
+ - spec/acfs/model/query_methods_spec.rb
228
240
  - spec/acfs/request/callbacks_spec.rb
229
241
  - spec/acfs/request_spec.rb
230
242
  - spec/acfs/response/formats_spec.rb
231
243
  - spec/acfs/service_spec.rb
232
244
  - spec/acfs_spec.rb
233
- - spec/model/attributes_spec.rb
234
- - spec/model/initialization_spec.rb
235
- - spec/model/locatable_spec.rb
236
245
  - spec/spec_helper.rb
237
246
  - spec/support/service.rb
238
247
  homepage: ''
@@ -262,13 +271,17 @@ summary: An abstract API base client for service oriented application.
262
271
  test_files:
263
272
  - spec/acfs/middleware/json_decoder_spec.rb
264
273
  - spec/acfs/middleware/msgpack_decoder_spec.rb
274
+ - spec/acfs/model/attributes_spec.rb
275
+ - spec/acfs/model/dirty_spec.rb
276
+ - spec/acfs/model/initialization_spec.rb
277
+ - spec/acfs/model/loadable_spec.rb
278
+ - spec/acfs/model/locatable_spec.rb
279
+ - spec/acfs/model/persistance_spec.rb
280
+ - spec/acfs/model/query_methods_spec.rb
265
281
  - spec/acfs/request/callbacks_spec.rb
266
282
  - spec/acfs/request_spec.rb
267
283
  - spec/acfs/response/formats_spec.rb
268
284
  - spec/acfs/service_spec.rb
269
285
  - spec/acfs_spec.rb
270
- - spec/model/attributes_spec.rb
271
- - spec/model/initialization_spec.rb
272
- - spec/model/locatable_spec.rb
273
286
  - spec/spec_helper.rb
274
287
  - spec/support/service.rb
@@ -1,16 +0,0 @@
1
- module Acfs
2
- class Service
3
-
4
- # Allows to queue request on this service that will be
5
- # delegated to global Acfs adapter queue.
6
- #
7
- module Queue
8
-
9
- # Queue a new request on global adapter queue.
10
- #
11
- def queue(request)
12
- Acfs.adapter.queue request
13
- end
14
- end
15
- end
16
- end
@@ -1,15 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe 'Acfs::Model::Locatable' do
4
- let(:model) { MyUser }
5
-
6
- describe '.url' do
7
- it 'should return URL' do
8
- expect(model.url).to be == 'http://users.example.org/users'
9
- end
10
-
11
- it 'should return URL with id path part if specified' do
12
- expect(model.url(5)).to be == 'http://users.example.org/users/5'
13
- end
14
- end
15
- end