acfs 0.7.0 → 0.8.0

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.
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