rhoconnect-rb 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.autotest ADDED
@@ -0,0 +1,3 @@
1
+ require "autotest/bundler"
2
+ require "autotest/fsevent"
3
+ require "autotest/growl"
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ coverage
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format nested
2
+ --color
3
+ spec/**/*_spec.rb
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
4
+ gem 'rake'
5
+
6
+ group :test do
7
+ gem 'rspec', '~>2.5.0', :require => 'spec'
8
+ gem 'rcov', '~>0.9.8'
9
+ gem 'webmock'
10
+ end
data/README.md ADDED
@@ -0,0 +1,50 @@
1
+ rhoconnect-rb
2
+ ===
3
+
4
+ A ruby client library for the [RhoSync](http://rhomobile.com/products/rhosync) App Integration Server.
5
+
6
+ Using rhoconnect-rb, your application's model data will transparently synchronize with a mobile application built using the [Rhodes framework](http://rhomobile.com/products/rhodes), or any of the available [RhoSync clients](http://rhomobile.com/products/rhosync/). This client includes built-in support for [ActiveRecord](http://ar.rubyonrails.org/) and [DataMapper](http://datamapper.org/) models.
7
+
8
+ ## Getting started
9
+
10
+ Load the `rhoconnect-rb` library:
11
+
12
+ require 'rhoconnect-rb'
13
+
14
+ Note, if you are using datamapper, install the `dm-serializer` library and require it in your application. `rhoconnect-rb` depends on this utility to interact with RhoSync applications using JSON.
15
+
16
+ ## Usage
17
+ Now include Rhosync::Resource in a model that you want to synchronize with your mobile application:
18
+
19
+ class Product < ActiveRecord::Base
20
+ include Rhosync::Resource
21
+ end
22
+
23
+ Or, if you are using DataMapper:
24
+
25
+ class Product
26
+ include DataMapper::Resource
27
+ include Rhosync::Resource
28
+ end
29
+
30
+ Next, your models will need to declare a partition key for `rhoconnect-rb`. This partition key is used by `rhoconnect-rb` to uniquely identify the model dataset when it is stored in a RhoSync application. It is typically an attribute on the model or related model. `rhoconnect-rb` supports two types of partitions:
31
+
32
+ * :app - No unique key will be used, a shared dataset is used for all users.
33
+ * lambda { some lambda } - Execute a lambda which returns the unique key string.
34
+
35
+ For example, the `Product` model above might have a `belongs_to :user` relationship. The partition identifying the username would be declared as:
36
+
37
+ class Product < ActiveRecord::Base
38
+ include Rhosync::Resource
39
+
40
+ belongs_to :user
41
+
42
+ partition lambda { self.user.username }
43
+ end
44
+
45
+ For more information about RhoSync partitions, please refer to the [RhoSync docs](http://docs.rhomobile.com/rhosync/source-adapters#data-partitioning).
46
+
47
+ ## Meta
48
+ Created and maintained by Vladimir Tarasov and Lars Burgess.
49
+
50
+ Released under the [MIT License](http://www.opensource.org/licenses/mit-license.php).
data/Rakefile ADDED
@@ -0,0 +1,21 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ Bundler.setup(:default, :test)
4
+ require 'bundler/gem_tasks'
5
+
6
+ require 'rspec/core/rake_task'
7
+
8
+ desc "Run all specs"
9
+ RSpec::Core::RakeTask.new(:spec) do |t|
10
+ t.rspec_opts = ["-b", "-c", "-fd"]
11
+ t.pattern = 'spec/**/*_spec.rb'
12
+ end
13
+
14
+ desc "Run all specs with rcov"
15
+ RSpec::Core::RakeTask.new(:rcov) do |t|
16
+ t.rcov = true
17
+ t.rspec_opts = ["-b", "-c", "-fd"]
18
+ t.rcov_opts = ['--exclude', 'spec/*,gems/*']
19
+ end
20
+
21
+ task :default => :spec
data/config/routes.rb ADDED
@@ -0,0 +1,7 @@
1
+ Rails.application.routes.draw do
2
+ match '/rhosync/authenticate' => Rhosync::Authenticate
3
+ match '/rhosync/query' => Rhosync::Query
4
+ match '/rhosync/create' => Rhosync::Create
5
+ match '/rhosync/update' => Rhosync::Update
6
+ match '/rhosync/delete' => Rhosync::Delete
7
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'rhoconnect-rb'
@@ -0,0 +1,7 @@
1
+ require 'json'
2
+ require 'rest_client'
3
+ require 'rhosync/version'
4
+ require 'rhosync/configuration'
5
+ require 'rhosync/client'
6
+ require 'rhosync/resource'
7
+ require 'rhosync/endpoints'
@@ -0,0 +1,88 @@
1
+ require 'uri'
2
+
3
+ module Rhosync
4
+ class Client
5
+ attr_accessor :uri, :token
6
+
7
+ # allow configuration, uri or environment variable initialization
8
+ def initialize(params = {})
9
+ uri = params[:uri] || Rhosync.configuration.uri || ENV['RHOSYNC_URL']
10
+ raise ArgumentError.new("Please provide a :uri or set RHOSYNC_URL") unless uri
11
+ uri = URI.parse(uri)
12
+
13
+ @token = params[:token] || Rhosync.configuration.token || uri.user
14
+ uri.user = nil; @uri = uri.to_s
15
+ raise ArgumentError.new("Please provide a :token or set it in uri") unless @token
16
+
17
+ RestClient.proxy = ENV['HTTP_PROXY'] || ENV['http_proxy']
18
+ end
19
+
20
+ def create(source_name, partition, obj = {})
21
+ send_objects(:push_objects, source_name, partition, obj)
22
+ end
23
+
24
+ def destroy(source_name, partition, obj = {})
25
+ send_objects(:push_deletes, source_name, partition, obj)
26
+ end
27
+
28
+ # update, create, it doesn't matter :)
29
+ alias :update :create
30
+
31
+ def set_auth_callback(callback)
32
+ process(:post, "/api/set_auth_callback", { :callback => callback })
33
+ end
34
+
35
+ def set_query_callback(source_name, callback)
36
+ process(:post, "/api/set_query_callback",
37
+ {
38
+ :source_id => source_name,
39
+ :callback => callback
40
+ }
41
+ )
42
+ end
43
+
44
+ protected
45
+
46
+ def validate_args(source_name, partition, obj = {}) # :nodoc:
47
+ raise ArgumentError.new("Missing object id for #{obj.inspect}") unless obj['id']
48
+ raise ArgumentError.new("Missing source_name.") unless source_name or source_name.empty?
49
+ raise ArgumentError.new("Missing partition for #{model}.") unless partition or partition.empty?
50
+ end
51
+
52
+ def send_objects(action, source_name, partition, obj = {}) # :nodoc:
53
+ validate_args(source_name, partition, obj)
54
+
55
+ process(:post, "/api/#{action}",
56
+ {
57
+ :source_id => source_name,
58
+ :user_id => partition,
59
+ :objects => action == :push_deletes ? [obj['id'].to_s] : { obj['id'] => obj }
60
+ }
61
+ )
62
+ end
63
+
64
+ def resource(path) # :nodoc:
65
+ RestClient::Resource.new(@uri)[path]
66
+ end
67
+
68
+ def process(method, path, payload = nil) # :nodoc:
69
+ headers = api_headers
70
+ unless method == :get
71
+ payload = payload.merge!(:api_token => @token).to_json
72
+ headers = api_headers.merge(:content_type => 'application/json')
73
+ end
74
+ args = [method, payload, headers].compact
75
+ response = resource(path).send(*args)
76
+ response
77
+ end
78
+
79
+ def api_headers # :nodoc:
80
+ {
81
+ 'User-Agent' => Rhosync::VERSION,
82
+ 'X-Ruby-Version' => RUBY_VERSION,
83
+ 'X-Ruby-Platform' => RUBY_PLATFORM
84
+ }
85
+ end
86
+
87
+ end
88
+ end
@@ -0,0 +1,38 @@
1
+ module Rhosync
2
+ class Configuration
3
+ attr_accessor :uri, :token, :authenticate, :sync_time_as_int
4
+
5
+ def initialize
6
+ @sync_time_as_int = true
7
+ end
8
+
9
+ end
10
+
11
+ class << self
12
+ attr_accessor :configuration
13
+ end
14
+
15
+ # Configure RhoSync in an initializer:
16
+ # like config/initializers/rhosync.rb
17
+ #
18
+ # Setup the RhoSync uri and api token.
19
+ # Use rhosync:get_token to get the token value.
20
+ #
21
+ # config.uri = "http://myrhosync.com"
22
+ # config.token = "secrettoken"
23
+ # config.authenticate = lambda { |credentials|
24
+ # User.authenticate(credentials)
25
+ # }
26
+ #
27
+ # @example
28
+ # Rhosync.configure do |config|
29
+ # config.uri = "http://myrhosync.com"
30
+ # config.token = "secrettoken"
31
+ # end
32
+ def self.configure
33
+ self.configuration = Configuration.new
34
+ yield(configuration)
35
+ end
36
+ end
37
+
38
+ Rhosync.configure { }
@@ -0,0 +1,167 @@
1
+ require 'json'
2
+
3
+ module Rhosync
4
+ class EndpointHelpers
5
+ def self.authenticate(content_type, body)
6
+ code, params = 200, parse_params(content_type, body)
7
+ if Rhosync.configuration.authenticate
8
+ code = 401 unless Rhosync.configuration.authenticate.call(params)
9
+ end
10
+ [code, {'Content-Type' => 'text/plain'}, [""]]
11
+ end
12
+
13
+ def self.query(content_type, body)
14
+ params = parse_params(content_type, body)
15
+ action, c_type, result, records = :rhosync_query, 'application/json', {}, []
16
+ # Call resource rhosync_query class method
17
+ code, error = get_rhosync_resource(params['resource'], action) do |klass|
18
+ records = klass.send(action, params['partition'])
19
+ end
20
+ if code == 200
21
+ # Serialize records into hash of hashes
22
+ records.each do |record|
23
+ result[record.id.to_s] = record.normalized_attributes
24
+ end
25
+ result = result.to_json
26
+ else
27
+ result = error
28
+ c_type = 'text/plain'
29
+ # Log warning if something broke
30
+ warn error
31
+ end
32
+ [code, {'Content-Type' => c_type}, [result]]
33
+ end
34
+
35
+ def self.on_cud(action, content_type, body)
36
+ params = parse_params(content_type, body)
37
+ object_id = ""
38
+ code, error = get_rhosync_resource(params['resource'], action) do |klass|
39
+ object_id = klass.send("rhosync_receive_#{action}".to_sym,
40
+ params['partition'], params['attributes'])
41
+ object_id = object_id.to_s if object_id
42
+ end
43
+ [code, {'Content-Type' => "text/plain"}, [error || object_id]]
44
+ end
45
+
46
+ def self.create(content_type, body)
47
+ self.on_cud(:create, content_type, body)
48
+ end
49
+
50
+ def self.update(content_type, body)
51
+ self.on_cud(:update, content_type, body)
52
+ end
53
+
54
+ def self.delete(content_type, body)
55
+ self.on_cud(:delete, content_type, body)
56
+ end
57
+
58
+ private
59
+
60
+ def self.get_rhosync_resource(resource_name, action)
61
+ code, error = 200, nil
62
+ begin
63
+ klass = Kernel.const_get(resource_name)
64
+ yield klass
65
+ rescue NoMethodError => ne
66
+ error = "error on method `#{action}` for #{resource_name}: #{ne.message}"
67
+ code = 404
68
+ rescue NameError
69
+ error = "Missing Rhosync::Resource #{resource_name}"
70
+ code = 404
71
+ # TODO: catch HaltException and Exception here, built-in source adapter will handle them
72
+ rescue Exception => e
73
+ error = e.message
74
+ code = 500
75
+ end
76
+ [code, error]
77
+ end
78
+
79
+ def self.parse_params(content_type, params)
80
+ if content_type and content_type.match(/^application\/json/) and params and params.length > 2
81
+ JSON.parse(params)
82
+ else
83
+ {}
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ # Detect if we're running inside of rails
90
+ if defined? Rails
91
+ #if Rails::VERSION::STRING.to_i >= 3
92
+ class Engine < Rails::Engine; end
93
+ #end
94
+
95
+ module Rhosync
96
+ class BaseEndpoint
97
+ def self.call(env)
98
+ req = Rack::Request.new(env)
99
+ Rhosync::EndpointHelpers.send(self.to_s.downcase.split("::")[1].to_sym, req.content_type, req.body.read)
100
+ end
101
+ end
102
+
103
+ class Authenticate < BaseEndpoint; end
104
+
105
+ class Query < BaseEndpoint; end
106
+
107
+ class Create < BaseEndpoint; end
108
+
109
+ class Update < BaseEndpoint; end
110
+
111
+ class Delete < BaseEndpoint; end
112
+ end
113
+ end
114
+
115
+
116
+ # Detect if we're running inside of sinatra
117
+ if defined? Sinatra
118
+ # Defines Sinatra routes
119
+ # This is automatically registered if you are using
120
+ # the 'classic' style sinatra application. To use in a
121
+ # classic application:
122
+ #
123
+ # require 'rubygems'
124
+ # require 'sinatra'
125
+ # require 'rhoconnect-rb'
126
+ #
127
+ # get '/' do
128
+ # 'hello world'
129
+ # end
130
+ #
131
+ # For modular sinatra applications, you will need to register
132
+ # the module inside your class. To use in a modular application:
133
+ #
134
+ # require 'sinatra/base'
135
+ # require 'rhoconnect-rb'
136
+ #
137
+ # class Myapp < Sinatra::Base
138
+ # register Sinatra::RhosyncEndpoints
139
+ # get '/' do
140
+ # 'hello world'
141
+ # end
142
+ # end
143
+ module Sinatra
144
+ module RhosyncHelpers
145
+ def call_helper(method,*args)
146
+ code, c_type, body = Rhosync::EndpointHelpers.send(method,*args)
147
+ content_type c_type['Content-Type']
148
+ status code
149
+ body[0]
150
+ end
151
+ end
152
+
153
+ module RhosyncEndpoints
154
+ def self.registered(app)
155
+ # install our endpoint helpers
156
+ app.send(:include, RhosyncHelpers)
157
+
158
+ [:authenticate,:query,:create,:update,:delete].each do |endpoint|
159
+ app.post "/rhosync/#{endpoint}" do
160
+ call_helper(endpoint, request.env['CONTENT_TYPE'], request.body.read)
161
+ end
162
+ end
163
+ end
164
+ end
165
+ register RhosyncEndpoints
166
+ end
167
+ end
@@ -0,0 +1,143 @@
1
+ module Rhosync
2
+ module Resource
3
+
4
+ def self.included(model)
5
+ model.extend(ClassMethods)
6
+ model.extend(HelperMethods)
7
+
8
+ model.send(:include, InstanceMethods)
9
+ model.send(:include, Callbacks)
10
+ end
11
+
12
+
13
+ module ClassMethods
14
+ def partition(p)
15
+ @partition = p
16
+ end
17
+
18
+ def get_partition
19
+ @partition.is_a?(Proc) ? @partition.call : @partition
20
+ end
21
+
22
+ def rhosync_receive_create(partition, attributes)
23
+ instance = self.send(:new)
24
+ instance.send(:rhosync_apply_attributes, partition, attributes)
25
+ instance.skip_rhosync_callbacks = true
26
+ instance.save
27
+ instance.id #=> return object id
28
+ end
29
+
30
+ def rhosync_receive_update(partition, attributes)
31
+ object_id = attributes.delete('id')
32
+ instance = self.send(is_datamapper? ? :get : :find, object_id)
33
+ instance.send(:rhosync_apply_attributes, partition, attributes)
34
+ instance.skip_rhosync_callbacks = true
35
+ instance.save
36
+ object_id
37
+ end
38
+
39
+ def rhosync_receive_delete(partition, attributes)
40
+ object_id = attributes['id']
41
+ instance = self.send(is_datamapper? ? :get : :find, object_id)
42
+ instance.skip_rhosync_callbacks = true
43
+ instance.destroy
44
+ object_id
45
+ end
46
+ end
47
+
48
+ module InstanceMethods
49
+ attr_accessor :skip_rhosync_callbacks
50
+
51
+ def rhosync_create
52
+ call_client_method(:create)
53
+ end
54
+
55
+ def rhosync_destroy
56
+ call_client_method(:destroy)
57
+ end
58
+
59
+ def rhosync_update
60
+ call_client_method(:update)
61
+ end
62
+
63
+ def rhosync_query(partition)
64
+ #return all objects for this partition
65
+ end
66
+
67
+ # By default we ignore partition
68
+ # TODO: Document - this is user-facing function
69
+ def rhosync_apply_attributes(partition, attributes)
70
+ self.attributes = attributes
71
+ end
72
+
73
+ # Return Rhosync-friendly attributes list
74
+ def normalized_attributes
75
+ attribs = self.attributes.dup
76
+ attribs.each do |key,value|
77
+ attribs[key] = Time.parse(value.to_s).to_i.to_s if value.is_a?(Time) or value.is_a?(DateTime)
78
+ end if Rhosync.configuration.sync_time_as_int
79
+ attribs
80
+ end
81
+
82
+ private
83
+
84
+ def call_client_method(action)
85
+ unless self.skip_rhosync_callbacks
86
+ attribs = self.normalized_attributes
87
+ begin
88
+ Rhosync::Client.new.send(action, self.class.to_s, self.class.get_partition, attribs)
89
+ rescue RestClient::Exception => re
90
+ warn "#{self.class.to_s}: rhosync_#{action} returned error: #{re.message} - #{re.http_body}"
91
+ rescue Exception => e
92
+ warn "#{self.class.to_s}: rhosync_#{action} returned unexpected error: #{e.message}"
93
+ end
94
+ end
95
+ end
96
+
97
+ end
98
+
99
+ module Callbacks
100
+
101
+ def self.included(model)
102
+ model.class_eval do
103
+ install_callbacks
104
+ end
105
+ end
106
+ end
107
+
108
+ module HelperMethods
109
+
110
+ def install_callbacks
111
+ if is_datamapper?
112
+ # test for dm-serializer
113
+ if not is_defined?(DataMapper::Serialize)
114
+ raise "Rhosync::Resource requires dm-serializer to work with DataMapper. Install with `gem install dm-serializer` and add to your application."
115
+ end
116
+ after :create, :rhosync_create
117
+ after :destroy, :rhosync_destroy
118
+ after :update, :rhosync_update
119
+ elsif is_activerecord?
120
+ after_create :rhosync_create
121
+ after_destroy :rhosync_destroy
122
+ after_update :rhosync_update
123
+ else
124
+ raise "Rhosync::Resource only supports ActiveRecord or DataMapper at this time."
125
+ end
126
+ end
127
+
128
+ private
129
+
130
+ def is_defined?(const) # :nodoc:
131
+ defined?(const)
132
+ end
133
+
134
+ def is_datamapper? # :nodoc:
135
+ self.included_modules.include?(DataMapper::Resource) rescue false
136
+ end
137
+
138
+ def is_activerecord? # :nodoc:
139
+ self.superclass == ActiveRecord::Base rescue false
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,3 @@
1
+ module Rhosync
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require 'rhosync/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "rhoconnect-rb"
7
+ s.version = Rhosync::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Rhomobile"]
10
+ s.date = Time.now.strftime('%Y-%m-%d')
11
+ s.email = ["support@rhomobile.com"]
12
+ s.homepage = %q{http://rhomobile.com}
13
+ s.summary = %q{RhoSync rails plugin}
14
+ s.description = %q{RhoSync rails plugin}
15
+
16
+ s.rubyforge_project = nil
17
+ s.add_dependency('rest-client', '~>1.6.1')
18
+ s.add_dependency('json', '~>1.4.6')
19
+
20
+ s.files = `git ls-files`.split("\n")
21
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
22
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
23
+ s.require_paths = ["lib"]
24
+
25
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
26
+
27
+ end
@@ -0,0 +1,125 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ describe Rhosync::Client do
4
+
5
+ context "on initialize" do
6
+ it "should initialize with RHOSYNC_URL environment var" do
7
+ ENV['RHOSYNC_URL'] = "http://token@test.rhosync.com"
8
+ c = Rhosync::Client.new
9
+ c.token.should == 'token'
10
+ c.uri.should == 'http://test.rhosync.com'
11
+ ENV.delete('RHOSYNC_URL')
12
+ end
13
+
14
+ it "should initialize with :uri parameter" do
15
+ c = Rhosync::Client.new(:uri => "http://token@test.rhosync.com")
16
+ c.token.should == 'token'
17
+ c.uri.should == 'http://test.rhosync.com'
18
+ end
19
+
20
+ it "should initialize with :token parameter" do
21
+ c = Rhosync::Client.new(:uri => "http://test.rhosync.com", :token => "token")
22
+ c.token.should == 'token'
23
+ c.uri.should == 'http://test.rhosync.com'
24
+ end
25
+
26
+ it "should initialize with configure block" do
27
+ Rhosync.configure do |config|
28
+ config.uri = "http://test.rhosync.com"
29
+ config.token = "token"
30
+ end
31
+ begin
32
+ c = Rhosync::Client.new
33
+ c.token.should == 'token'
34
+ c.uri.should == 'http://test.rhosync.com'
35
+ ensure
36
+ Rhosync.configure do |config|
37
+ config.uri = nil
38
+ config.token = nil
39
+ end
40
+ end
41
+ end
42
+
43
+ it "should raise ArgumentError if uri is missing" do
44
+ lambda { Rhosync::Client.new }.should raise_error(ArgumentError, "Please provide a :uri or set RHOSYNC_URL")
45
+ end
46
+
47
+ it "should raise ArugmentError if token is missing" do
48
+ lambda {
49
+ Rhosync::Client.new(:uri => "http://test.rhosync.com")
50
+ }.should raise_error(ArgumentError, "Please provide a :token or set it in uri")
51
+ end
52
+ end
53
+
54
+ context "on create update destroy" do
55
+ before(:each) do
56
+ @client = Rhosync::Client.new(:uri => "http://token@test.rhosync.com")
57
+ end
58
+
59
+ it "should create an object" do
60
+ stub_request(:post, "http://test.rhosync.com/api/push_objects").with(
61
+ :headers => {"Content-Type" => "application/json"}
62
+ ).to_return(:status => 200, :body => "done")
63
+ resp = @client.create("Person", "user1",
64
+ {
65
+ 'id' => 1,
66
+ 'name' => 'user1'
67
+ }
68
+ )
69
+ resp.body.should == "done"
70
+ resp.code.should == 200
71
+ end
72
+
73
+ it "should update an object" do
74
+ stub_request(:post, "http://test.rhosync.com/api/push_objects").with(
75
+ :headers => {"Content-Type" => "application/json"}
76
+ ).to_return(:status => 200, :body => "done")
77
+ resp = @client.update("Person", "user1",
78
+ {
79
+ 'id' => 1,
80
+ 'name' => 'user1'
81
+ }
82
+ )
83
+ resp.body.should == "done"
84
+ resp.code.should == 200
85
+ end
86
+
87
+ it "should destroy an object" do
88
+ stub_request(:post, "http://test.rhosync.com/api/push_deletes").with(
89
+ :headers => {"Content-Type" => "application/json"}
90
+ ).to_return(:status => 200, :body => "done")
91
+ resp = @client.destroy("Person", "user1",
92
+ {
93
+ 'id' => 1,
94
+ 'name' => 'user1'
95
+ }
96
+ )
97
+ resp.body.should == "done"
98
+ resp.code.should == 200
99
+ end
100
+ end
101
+
102
+ context "on set callbacks" do
103
+ before(:each) do
104
+ @client = Rhosync::Client.new(:uri => "http://token@test.rhosync.com")
105
+ end
106
+
107
+ it "should set auth callback" do
108
+ stub_request(:post, "http://test.rhosync.com/api/set_auth_callback").with(
109
+ :headers => {"Content-Type" => "application/json"}
110
+ ).to_return(:status => 200, :body => "done")
111
+ resp = @client.set_auth_callback("http://example.com/callback")
112
+ resp.body.should == "done"
113
+ resp.code.should == 200
114
+ end
115
+
116
+ it "should set query callback" do
117
+ stub_request(:post, "http://test.rhosync.com/api/set_query_callback").with(
118
+ :headers => {"Content-Type" => "application/json"}
119
+ ).to_return(:status => 200, :body => "done")
120
+ resp = @client.set_query_callback("Person", "http://example.com/callback")
121
+ resp.body.should == "done"
122
+ resp.code.should == 200
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,234 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ describe Rhosync::EndpointHelpers do
4
+
5
+ # Auth stub class
6
+ class AuthTest; end
7
+ class BrokenResource < ActiveRecord::Base
8
+ include Rhosync::Resource
9
+ end
10
+
11
+ # Query stub class
12
+ class Product < ActiveRecord::Base
13
+ include Rhosync::Resource
14
+ def self.rhosync_query(partition)
15
+ [self.new]
16
+ end
17
+ end
18
+
19
+ def setup_auth_test(success)
20
+ AuthTest.stub!(:do_auth).and_return(success)
21
+ AuthTest.should_receive(:do_auth).with(@creds)
22
+
23
+ Rhosync.configure do |config|
24
+ config.uri = "http://test.rhosync.com"
25
+ config.token = "token"
26
+ config.authenticate = lambda {|credentials|
27
+ AuthTest.do_auth(credentials)
28
+ }
29
+ end
30
+ end
31
+
32
+ before(:all) do
33
+ @params = {'partition' => 'testuser', 'resource' => 'Product'}
34
+ @creds = {'user' => 'john', 'pass' => 'secret'}
35
+ end
36
+
37
+ context "on Rails auth endpoint" do
38
+ before(:each) do
39
+ strio = mock("StringIO")
40
+ strio.stub!(:read).and_return(JSON.generate(@creds))
41
+ @env = mock("env")
42
+ @env.stub!(:body).and_return(strio)
43
+ @env.stub!(:content_type).and_return('application/json')
44
+ Rack::Request.stub!(:new).and_return(@env)
45
+ end
46
+
47
+ it "should call configured authenticate block" do
48
+ setup_auth_test(true)
49
+ Rhosync::Authenticate.call(@env).should == [
50
+ 200, {'Content-Type' => 'text/plain'}, [""]
51
+ ]
52
+ end
53
+
54
+ it "should call configured authenticate block with 401" do
55
+ setup_auth_test(false)
56
+ Rhosync::Authenticate.call(@env).should == [
57
+ 401, {'Content-Type' => 'text/plain'}, [""]
58
+ ]
59
+ end
60
+
61
+ it "should return true if no authenticate block exists" do
62
+ Rhosync.configure do |config|
63
+ config.uri = "http://test.rhosync.com"
64
+ config.token = "token"
65
+ end
66
+ Rhosync.configuration.authenticate.should be_nil
67
+ Rhosync::Authenticate.call(@env).should == [
68
+ 200, {'Content-Type' => 'text/plain'}, [""]
69
+ ]
70
+ end
71
+
72
+ it "should call authenticate block with empty params" do
73
+ Rhosync::EndpointHelpers.authenticate('text/plain', '').should == [
74
+ 200, {"Content-Type"=>"text/plain"}, [""]
75
+ ]
76
+ end
77
+ end
78
+
79
+ context "on Create/Update/Delete/Query endpoints" do
80
+ before(:each) do
81
+ @strio = mock("StringIO")
82
+ @env = mock("env")
83
+ @env.stub!(:content_type).and_return('application/json')
84
+ end
85
+
86
+ it "should call query endpoint" do
87
+ @strio.stub!(:read).and_return(
88
+ {'partition' => 'testuser', 'resource' => 'Product'}.to_json
89
+ )
90
+ @env.stub!(:body).and_return(@strio)
91
+ Rack::Request.stub!(:new).and_return(@env)
92
+ code, content_type, body = Rhosync::Query.call(@env)
93
+ code.should == 200
94
+ content_type.should == { "Content-Type" => "application/json" }
95
+ JSON.parse(body[0]).should == { '1' => Product.new.normalized_attributes }
96
+ end
97
+
98
+ it "should fail on missing Rhosync::Resource" do
99
+ @strio.stub!(:read).and_return(
100
+ {'partition' => 'testuser', 'resource' => 'Broken'}.to_json
101
+ )
102
+ @env.stub!(:body).and_return(@strio)
103
+ Rack::Request.stub!(:new).and_return(@env)
104
+ code, content_type, body = Rhosync::Query.call(@env)
105
+ code.should == 404
106
+ content_type.should == { "Content-Type" => "text/plain" }
107
+ body[0].should == "Missing Rhosync::Resource Broken"
108
+ end
109
+
110
+ it "should fail on undefined rhosync_query method" do
111
+ @strio.stub!(:read).and_return(
112
+ {'partition' => 'testuser', 'resource' => 'BrokenResource'}.to_json
113
+ )
114
+ @env.stub!(:body).and_return(@strio)
115
+ Rack::Request.stub!(:new).and_return(@env)
116
+ code, content_type, body = Rhosync::Query.call(@env)
117
+ code.should == 404
118
+ content_type.should == { "Content-Type" => "text/plain" }
119
+ body[0].should == "error on method `rhosync_query` for BrokenResource: undefined method `rhosync_query' for BrokenResource:Class"
120
+ end
121
+
122
+ it "should fail on unknown exception" do
123
+ @strio.stub!(:read).and_return(
124
+ {'partition' => 'testuser', 'resource' => 'Product'}.to_json
125
+ )
126
+ @env.stub!(:body).and_return(@strio)
127
+ Rack::Request.stub!(:new).and_return(@env)
128
+ Product.stub!(:rhosync_receive_create).and_return { raise "error in create" }
129
+ code, content_type, body = Rhosync::Create.call(@env)
130
+ code.should == 500
131
+ content_type.should == { "Content-Type" => "text/plain" }
132
+ body[0].should == "error in create"
133
+ end
134
+
135
+ it "should call create endpoint" do
136
+ params = {
137
+ 'resource' => 'Product',
138
+ 'partition' => 'app',
139
+ 'attributes' => {
140
+ 'name' => 'iphone',
141
+ 'brand' => 'apple'
142
+ }
143
+ }
144
+ @strio.stub!(:read).and_return(params.to_json)
145
+ @env.stub!(:body).and_return(@strio)
146
+ Rack::Request.stub!(:new).and_return(@env)
147
+ code, content_type, body = Rhosync::Create.call(@env)
148
+ code.should == 200
149
+ content_type.should == { "Content-Type" => "text/plain" }
150
+ body.should == ['1']
151
+ end
152
+
153
+ it "should call update endpoint" do
154
+ params = {
155
+ 'resource' => 'Product',
156
+ 'partition' => 'app',
157
+ 'attributes' => {
158
+ 'id' => '123',
159
+ 'name' => 'iphone',
160
+ 'brand' => 'apple'
161
+ }
162
+ }
163
+ @strio.stub!(:read).and_return(params.to_json)
164
+ @env.stub!(:body).and_return(@strio)
165
+ Rack::Request.stub!(:new).and_return(@env)
166
+ code, content_type, body = Rhosync::Update.call(@env)
167
+ code.should == 200
168
+ content_type.should == { "Content-Type" => "text/plain" }
169
+ body.should == ["123"]
170
+ end
171
+
172
+ it "should call delete endpoint" do
173
+ params = {
174
+ 'resource' => 'Product',
175
+ 'partition' => 'app',
176
+ 'attributes' => {
177
+ 'id' => '123',
178
+ 'name' => 'iphone',
179
+ 'brand' => 'apple'
180
+ }
181
+ }
182
+ @strio.stub!(:read).and_return(params.to_json)
183
+ @env.stub!(:body).and_return(@strio)
184
+ Rack::Request.stub!(:new).and_return(@env)
185
+ code, content_type, body = Rhosync::Delete.call(@env)
186
+ code.should == 200
187
+ content_type.should == { "Content-Type" => "text/plain" }
188
+ body.should == ["123"]
189
+ end
190
+
191
+ end
192
+
193
+ context "on Sinatra endpoints" do
194
+ class EndpointTest
195
+ include Sinatra::RhosyncHelpers
196
+ end
197
+
198
+ it "should register endpoints for authenticate and query" do
199
+ strio = mock("StringIO")
200
+ strio.stub!(:read).and_return(@creds.to_json)
201
+ req = mock("request")
202
+ req.stub!(:body).and_return(strio)
203
+ req.stub!(:env).and_return('CONTENT_TYPE' => 'application/json')
204
+ Sinatra::RhosyncEndpoints.stub!(:request).and_return(req)
205
+ Sinatra::RhosyncEndpoints.stub!(:params).and_return(@params)
206
+ Rhosync::EndpointHelpers.stub!(:query)
207
+ app = mock("app")
208
+ app.stub!(:post).and_yield
209
+ app.should_receive(:post).exactly(5).times
210
+ app.should_receive(:include).with(Sinatra::RhosyncHelpers)
211
+ Sinatra::RhosyncEndpoints.should_receive(:call_helper).exactly(5).times
212
+ Sinatra::RhosyncEndpoints.registered(app)
213
+ end
214
+
215
+ it "should call helper for authenticate" do
216
+ app = EndpointTest.new
217
+ app.should_receive(:status).with(200)
218
+ app.should_receive(:content_type).with('text/plain')
219
+ app.call_helper(
220
+ :authenticate, 'application/json', @creds.to_json
221
+ ).should == ""
222
+ end
223
+
224
+ it "should call helper for query" do
225
+ app = EndpointTest.new
226
+ app.should_receive(:status).with(200)
227
+ app.should_receive(:content_type).with('application/json')
228
+ result = app.call_helper(
229
+ :query, 'application/json', @params.to_json
230
+ )
231
+ JSON.parse(result).should == { '1' => Product.new.normalized_attributes }
232
+ end
233
+ end
234
+ end
@@ -0,0 +1,118 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ describe Rhosync::Resource do
4
+
5
+ context "on set partition" do
6
+ it "should set resource partition to :app" do
7
+ class TestModel1 < ActiveRecord::Base
8
+ include Rhosync::Resource
9
+
10
+ partition :app
11
+ end
12
+
13
+ TestModel1.get_partition.should == :app
14
+ end
15
+
16
+ it "should set resource partition with lambda" do
17
+ class TestModel2 < ActiveRecord::Base
18
+ include Rhosync::Resource
19
+
20
+ partition lambda{ 'helloworld' }
21
+ end
22
+
23
+ TestModel2.get_partition.should == 'helloworld'
24
+ end
25
+ end
26
+
27
+ context "on initialize" do
28
+ it "should raise exception if DataMapper or ActiveRecord::Base are missing" do
29
+ lambda { class TestModel3
30
+ include Rhosync::Resource
31
+ end
32
+ }.should raise_error("Rhosync::Resource only supports ActiveRecord or DataMapper at this time.")
33
+ end
34
+
35
+ it "should register callbacks for ActiveRecord::Base" do
36
+ class TestModel4 < ActiveRecord::Base
37
+ include Rhosync::Resource
38
+ end
39
+
40
+ TestModel4.create_callback.should == :rhosync_create
41
+ TestModel4.destroy_callback.should == :rhosync_destroy
42
+ TestModel4.update_callback.should == :rhosync_update
43
+ end
44
+
45
+ it "should register callbacks for DataMapper::Resource" do
46
+ class TestModel5
47
+ include DataMapper::Resource
48
+ include Rhosync::Resource
49
+ end
50
+
51
+ TestModel5.rhosync_callbacks[:create].should == :rhosync_create
52
+ TestModel5.rhosync_callbacks[:destroy].should == :rhosync_destroy
53
+ TestModel5.rhosync_callbacks[:update].should == :rhosync_update
54
+ end
55
+
56
+ it "should raise exception if dm-serializer is missing" do
57
+ class TestModel6
58
+ include DataMapper::Resource
59
+ include Rhosync::Resource
60
+ end
61
+ TestModel6.stub!(:is_defined?).and_return(false)
62
+ lambda {
63
+ TestModel6.install_callbacks
64
+ }.should raise_error("Rhosync::Resource requires dm-serializer to work with DataMapper. Install with `gem install dm-serializer` and add to your application.")
65
+ end
66
+ end
67
+
68
+ context "on create update delete" do
69
+
70
+ it "should call create update delete hook" do
71
+ class TestModel7 < ActiveRecord::Base
72
+ include Rhosync::Resource
73
+ partition :app
74
+ end
75
+ client = mock('Rhosync::Client')
76
+ client.stub!(:send)
77
+ Rhosync::Client.stub!(:new).and_return(client)
78
+ [:create, :update, :destroy].each do |action|
79
+ client.should_receive(:send).with(
80
+ action, "TestModel7", :app, {"name"=>"John", "created_at"=>"1299636666", "updated_at"=>"1299636666", "id"=>1}
81
+ )
82
+ TestModel7.new.send("rhosync_#{action}".to_sym)
83
+ end
84
+ end
85
+
86
+ it "should warn on RestClient::Exception" do
87
+ class TestModel8 < ActiveRecord::Base
88
+ include Rhosync::Resource
89
+ partition :app
90
+ end
91
+ client = mock('Rhosync::Client')
92
+ exception = RestClient::Exception.new(
93
+ RestClient::Response.create("error connecting to server", nil, nil), 500
94
+ )
95
+ exception.message = "Internal Server Error"
96
+ client.stub!(:send).and_return { raise exception }
97
+ Rhosync::Client.stub!(:new).and_return(client)
98
+ tm = TestModel8.new
99
+ tm.should_receive(:warn).with(
100
+ "TestModel8: rhosync_create returned error: Internal Server Error - error connecting to server"
101
+ )
102
+ tm.rhosync_create
103
+ end
104
+
105
+ it "should warn on Exception" do
106
+ class TestModel8 < ActiveRecord::Base
107
+ include Rhosync::Resource
108
+ partition :app
109
+ end
110
+ client = mock('Rhosync::Client')
111
+ client.stub!(:send).and_return { raise Exception.new("error connecting to server") }
112
+ Rhosync::Client.stub!(:new).and_return(client)
113
+ tm = TestModel8.new
114
+ tm.should_receive(:warn).with("TestModel8: rhosync_create returned unexpected error: error connecting to server")
115
+ tm.rhosync_create
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,103 @@
1
+ $:.unshift File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ require 'rspec'
3
+ require 'webmock/rspec'
4
+
5
+ include WebMock::API
6
+
7
+ # stub for rack
8
+ module Rack
9
+ class Request; end
10
+ end
11
+
12
+ # stubs for rails engine
13
+ module Rails
14
+ class Engine; end
15
+ end
16
+
17
+ # stubs for sinatra
18
+ module Sinatra
19
+ def self.register(mod); end
20
+ module RhosyncEndpoints
21
+ def self.content_type(c_type); end
22
+ def self.status(code); end
23
+ end
24
+ end
25
+
26
+ require 'rhoconnect-rb'
27
+
28
+
29
+ # define ActiveRecord and DM here for testing
30
+ module ActiveRecord
31
+ class Base
32
+
33
+ def attributes
34
+ {
35
+ "name" => "John",
36
+ "created_at" => Time.parse("Wed Mar 09 02:11:06 UTC 2011"),
37
+ "updated_at" => Time.parse("Wed Mar 09 02:11:06 UTC 2011"),
38
+ "id" => 1
39
+ }
40
+ end
41
+
42
+ def attributes=(attribs); end
43
+
44
+ def id; 1 end
45
+
46
+ def warn(*args)
47
+ Kernel.warn(args)
48
+ end
49
+
50
+ def save; end
51
+
52
+ def self.find(object_id)
53
+ self.new
54
+ end
55
+
56
+ def destroy; end
57
+
58
+ class << self
59
+ attr_accessor :create_callback,:destroy_callback,:update_callback
60
+
61
+ def after_create(callback)
62
+ @create_callback = callback
63
+ end
64
+
65
+ def after_destroy(callback)
66
+ @destroy_callback = callback
67
+ end
68
+
69
+ def after_update(callback)
70
+ @update_callback = callback
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ module DataMapper
77
+ module Resource
78
+
79
+ def attributes
80
+ {
81
+ :created_at => DateTime.parse("Wed Mar 09 02:11:06 UTC 2011"),
82
+ :updated_at => DateTime.parse("Wed Mar 09 02:11:06 UTC 2011"),
83
+ :name => "John",
84
+ :id => 1
85
+ }
86
+ end
87
+
88
+ def self.included(model)
89
+ model.extend(ClassMethods)
90
+ end
91
+
92
+ module ClassMethods
93
+ attr_accessor :rhosync_callbacks
94
+
95
+ def after(action, callback)
96
+ @rhosync_callbacks ||= {}
97
+ @rhosync_callbacks[action] = callback
98
+ end
99
+ end
100
+ end
101
+
102
+ module Serialize; end
103
+ end
metadata ADDED
@@ -0,0 +1,118 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rhoconnect-rb
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - Rhomobile
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-06-08 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ type: :runtime
22
+ requirement: &id001 !ruby/object:Gem::Requirement
23
+ none: false
24
+ requirements:
25
+ - - ~>
26
+ - !ruby/object:Gem::Version
27
+ hash: 13
28
+ segments:
29
+ - 1
30
+ - 6
31
+ - 1
32
+ version: 1.6.1
33
+ version_requirements: *id001
34
+ name: rest-client
35
+ prerelease: false
36
+ - !ruby/object:Gem::Dependency
37
+ type: :runtime
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ hash: 11
44
+ segments:
45
+ - 1
46
+ - 4
47
+ - 6
48
+ version: 1.4.6
49
+ version_requirements: *id002
50
+ name: json
51
+ prerelease: false
52
+ description: RhoSync rails plugin
53
+ email:
54
+ - support@rhomobile.com
55
+ executables: []
56
+
57
+ extensions: []
58
+
59
+ extra_rdoc_files: []
60
+
61
+ files:
62
+ - .autotest
63
+ - .gitignore
64
+ - .rspec
65
+ - Gemfile
66
+ - README.md
67
+ - Rakefile
68
+ - config/routes.rb
69
+ - init.rb
70
+ - lib/rhoconnect-rb.rb
71
+ - lib/rhosync/client.rb
72
+ - lib/rhosync/configuration.rb
73
+ - lib/rhosync/endpoints.rb
74
+ - lib/rhosync/resource.rb
75
+ - lib/rhosync/version.rb
76
+ - rhoconnect-rb.gemspec
77
+ - spec/client_spec.rb
78
+ - spec/endpoints_spec.rb
79
+ - spec/resource_spec.rb
80
+ - spec/spec_helper.rb
81
+ homepage: http://rhomobile.com
82
+ licenses: []
83
+
84
+ post_install_message:
85
+ rdoc_options: []
86
+
87
+ require_paths:
88
+ - lib
89
+ required_ruby_version: !ruby/object:Gem::Requirement
90
+ none: false
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ hash: 3
95
+ segments:
96
+ - 0
97
+ version: "0"
98
+ required_rubygems_version: !ruby/object:Gem::Requirement
99
+ none: false
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ hash: 3
104
+ segments:
105
+ - 0
106
+ version: "0"
107
+ requirements: []
108
+
109
+ rubyforge_project:
110
+ rubygems_version: 1.8.5
111
+ signing_key:
112
+ specification_version: 3
113
+ summary: RhoSync rails plugin
114
+ test_files:
115
+ - spec/client_spec.rb
116
+ - spec/endpoints_spec.rb
117
+ - spec/resource_spec.rb
118
+ - spec/spec_helper.rb