rhoconnect-rb 0.1.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.
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