diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..316c9be --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +.rspec +pkg +vendor \ No newline at end of file diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..481baea --- /dev/null +++ b/Gemfile @@ -0,0 +1,8 @@ +source :rubygems + +gem 'rake' +gemspec + +group :test do + gem 'rspec' +end \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..8e5fc77 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,45 @@ +PATH + remote: . + specs: + crowdtilt (0.0.1) + activemodel + faraday + faraday_middleware + json + +GEM + remote: http://rubygems.org/ + specs: + activemodel (3.2.11) + activesupport (= 3.2.11) + builder (~> 3.0.0) + activesupport (3.2.11) + i18n (~> 0.6) + multi_json (~> 1.0) + builder (3.0.4) + diff-lcs (1.1.3) + faraday (0.8.4) + multipart-post (~> 1.1) + faraday_middleware (0.8.8) + faraday (>= 0.7.4, < 0.9) + i18n (0.6.1) + json (1.7.6) + multi_json (1.5.0) + multipart-post (1.1.5) + rake (10.0.3) + rspec (2.12.0) + rspec-core (~> 2.12.0) + rspec-expectations (~> 2.12.0) + rspec-mocks (~> 2.12.0) + rspec-core (2.12.2) + rspec-expectations (2.12.1) + diff-lcs (~> 1.1.3) + rspec-mocks (2.12.1) + +PLATFORMS + ruby + +DEPENDENCIES + crowdtilt! + rake + rspec diff --git a/README.md b/README.md new file mode 100644 index 0000000..8091bd9 --- /dev/null +++ b/README.md @@ -0,0 +1,97 @@ +# Crowdtilt Ruby Library + +## Introduction + +This is a very rudimentary shot at implementing the Crowdtilt API. Feel free to add support for missing ones. + +While continued development happens on the Crowdtilt API, I opted to forego specs conforming to the API schema since determining when changes happened were easier if the HTTP call blew up. This should really be fixed in the future once the schema solidifies. + +## Usage + +First start by configuring Crowdtilt: + +``` +Crowdtilt.configure do + key "KEY" + secret "SECRET" + env "production" # not setting this will default to "development" +end +``` + +If you're using Rails: + +``` +Crowdtilt.configure do + key "KEY" + secret "SECRET" + env Rails.env +end +``` + +You should be good to go. Example usage: + +``` +# Create a user +u = Crowdtilt::User.new(:name => 'Ian', :email => 'ian@example.org') +u.persisted? #=> false +u.save +u.persisted? #=> true + +# List all users +Crowdtilt::User.all +#=> [Crowdtilt::User,...] + +c = u.campaigns.create "title" => "Foo", + "description" => "Bar", + "expiration_date" => 2.weeks.from_now, + "tilt_amount" => 1000 +``` + +## Issues + +Bound to be some issues since there's barely any specs. Feel free to submit an issue. + +## Endpoints Supported + +[✓] POST /users +[✓] POST /users/:id/verification +[ ] GET /users/authentication?email=x&password=y +[✓] GET /users/:id +[✓] GET /users +[✓] PUT /users/:id +[✓] GET /users/:id/campaigns +[e] GET /users/:id/campaigns/:id +[ ] GET /users/:id/paid_campaigns +[✓] POST /users/:id/cards +[✓] GET /users/:id/cards/:id +[✓] GET /users/:id/cards +[✓] PUT /users/:id/cards/:id +[✓] DELETE /users/:id/cards/:id +[✓] POST /users/:id/banks +[✓] GET /users/:id/banks/:id +[✓] GET /users/:id/banks +[✓] PUT /users/:id/banks/:id +[✓] DELETE /users/:id/banks/:id +[✓] GET /users/:id/payments +[✓] POST /campaigns +[✓] GET /campaigns/:id +[✓] GET /campaigns +[✓] PUT /campaigns/:id +[✓] POST /campaigns/:id/payments +[✓] GET /campaigns/:id/payments/:id +[✓] PUT /campaigns/:id/payments/:id +[✓] GET /campaigns/:id/payments +[ ] GET /campaigns/:id/rejected_payments +[ ] POST /campaigns/:id/payments/:id/refund +[ ] GET /campaigns/:id/settlements +[ ] GET /campaigns/:id/settlements/:id +[ ] POST /campaigns/:id/settlements/:id/bank +[ ] POST /campaigns/:id/comments +[ ] GET /campaigns/:id/comments +[ ] GET /campaigns/:id/comments/:id +[ ] PUT /campaigns/:id/comments/:id +[ ] DELETE /campaigns/:id/comments/:id + +## Todo + +* The association-like chaining executes the all http call before doing the find, or other method calls. Lazy eval the associated array so we don't do 2 calls. \ No newline at end of file diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..d62f2f6 --- /dev/null +++ b/Rakefile @@ -0,0 +1,15 @@ +require 'rspec/core/rake_task' +require './lib/crowdtilt/version' + +RSpec::Core::RakeTask.new(:spec) + +task :default => :spec + +task :build do + system "gem build crowdtilt.gemspec" +end + +task :release => :build do + `fury push crowdtilt-#{Zaarly::Geolocation::VERSION}.gem` + `rm crowdtilt-#{Zaarly::Geolocation::VERSION}.gem` +end \ No newline at end of file diff --git a/crowdtilt.gemspec b/crowdtilt.gemspec new file mode 100644 index 0000000..afb1af7 --- /dev/null +++ b/crowdtilt.gemspec @@ -0,0 +1,26 @@ +# -*- encoding: utf-8 -*- +$:.push File.expand_path("../lib", __FILE__) +require "crowdtilt/version" + +Gem::Specification.new do |s| + s.name = "crowdtilt" + s.version = Crowdtilt::VERSION + s.platform = Gem::Platform::RUBY + s.authors = ["Ian Hunter"] + s.email = ["ianhunter@gmail.com"] + s.homepage = "https://github.com/ihunter/crowdtilt" + s.summary = "Crowdtilt API library" + s.description = "Allows access to the Crowdtilt public API" + + s.rubyforge_project = s.name + + s.files = `git ls-files`.split("\n") + s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") + s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } + s.require_paths = ["lib"] + + s.add_dependency 'activemodel' + s.add_dependency 'faraday' + s.add_dependency 'faraday_middleware' + s.add_dependency 'json' +end diff --git a/lib/crowdtilt.rb b/lib/crowdtilt.rb new file mode 100644 index 0000000..b398dec --- /dev/null +++ b/lib/crowdtilt.rb @@ -0,0 +1,74 @@ +require 'active_model' +require 'faraday' +require 'faraday_middleware' +require 'json' + +require File.dirname(__FILE__) + "/crowdtilt/config" +require File.dirname(__FILE__) + "/crowdtilt/model" +require File.dirname(__FILE__) + "/crowdtilt/bank" +require File.dirname(__FILE__) + "/crowdtilt/campaign" +require File.dirname(__FILE__) + "/crowdtilt/card" +require File.dirname(__FILE__) + "/crowdtilt/user" + +module Crowdtilt + + class << self + def configure(&block) + @config = Crowdtilt::Config.new(&block) + end + + def config + if @config.nil? + raise "Crowdtilt not initialize, please configure using Crowdtilt.configure" + end + @config + end + + def request(method,*args) + conn = Faraday.new(:url => config.url) do |faraday| + # faraday.response :logger + faraday.request :json + faraday.response :json, :content_type => /\bjson$/ + faraday.use :instrumentation + + faraday.adapter Faraday.default_adapter + end + conn.basic_auth(config.key, config.secret) + conn.headers.update({'Content-Type' => 'application/json'}) + + res = conn.send method.to_sym, *args + + puts + puts "#{method.to_s.upcase} #{args[0]} #{args[1]}" + puts "Response #{res.status}" + puts res.body if res.body + puts + + case res.status + when 400...499 + raise res.body['error'] + when 500...599 + # prob want to handle this differently + raise res.body['error'] + else + res + end + end + + def get(path) + request :get, "/v1#{path}" + end + + def post(path, params={}) + request :post, "/v1#{path}", params + end + + def put(path, params={}) + request :put, "/v1#{path}", params + end + + def delete(path) + request :delete, "/v1#{path}" + end + end +end \ No newline at end of file diff --git a/lib/crowdtilt/README.md b/lib/crowdtilt/README.md new file mode 100644 index 0000000..78c34ee --- /dev/null +++ b/lib/crowdtilt/README.md @@ -0,0 +1,47 @@ += Endpoints +[✓] POST /users +[✓] POST /users/:id/verification +[ ] GET /users/authentication?email=x&password=y +[✓] GET /users/:id +[✓] GET /users +[✓] PUT /users/:id +[✓] GET /users/:id/campaigns +[e] GET /users/:id/campaigns/:id + This returns 200 status when it's actually 404 +[ ] GET /users/:id/paid_campaigns +[✓] POST /users/:id/cards +[✓] GET /users/:id/cards/:id +[✓] GET /users/:id/cards +[✓] PUT /users/:id/cards/:id +[✓] DELETE /users/:id/cards/:id +[✓] POST /users/:id/banks +[✓] GET /users/:id/banks/:id +[✓] GET /users/:id/banks +[✓] PUT /users/:id/banks/:id +[✓] DELETE /users/:id/banks/:id +[✓] GET /users/:id/payments +[✓] POST /campaigns +[✓] GET /campaigns/:id +[✓] GET /campaigns +[✓] PUT /campaigns/:id +[✓] POST /campaigns/:id/payments + This returns campaign_id, user_id, card_id, not object hashes +[✓] GET /campaigns/:id/payments/:id +[✓] PUT /campaigns/:id/payments/:id +[✓] GET /campaigns/:id/payments +[ ] GET /campaigns/:id/rejected_payments +[ ] POST /campaigns/:id/payments/:id/refund +[ ] GET /campaigns/:id/settlements +[ ] GET /campaigns/:id/settlements/:id +[ ] POST /campaigns/:id/settlements/:id/bank +[ ] POST /campaigns/:id/comments +[ ] GET /campaigns/:id/comments +[ ] GET /campaigns/:id/comments/:id +[ ] PUT /campaigns/:id/comments/:id +[ ] DELETE /campaigns/:id/comments/:id + += Questions +* Is there no delete user endpoint? + += Todo +* The association-like chaining executes the all http call before doing the find, or other method calls. Lazy eval the associated array so we don't do 2 calls. \ No newline at end of file diff --git a/lib/crowdtilt/bank.rb b/lib/crowdtilt/bank.rb new file mode 100644 index 0000000..ec48291 --- /dev/null +++ b/lib/crowdtilt/bank.rb @@ -0,0 +1,41 @@ +module Crowdtilt + class Bank < Model + attr_accessor :account_number, :name, :bank_code, :account_number_last_four, + :bank_code_last_four, :id, :is_valid, :metadata, :name, :user, + :user_uri, :uri + + uri_prefix '/users/#{user.id}/banks' + + coerce :user => 'Crowdtilt::User' + + def create_json + { "bank" => { "account_number" => account_number, + "name" => name, + "bank_code" => bank_code } } + end + + def update_json + { "bank" => { "metadata" => metadata } } + end + end + + class BanksArray < Array + attr_reader :user + def initialize(user, banks) + super banks + @user = user + end + + def find(id) + Crowdtilt::Bank.new Crowdtilt.get("/users/#{user.id}/banks/"+id).body['bank'].merge(:user => user) + end + + def build(params) + Crowdtilt::Bank.new params.merge(:user => user.as_json) + end + + def create(params) + build(params).save + end + end +end \ No newline at end of file diff --git a/lib/crowdtilt/campaign.rb b/lib/crowdtilt/campaign.rb new file mode 100644 index 0000000..c418ddd --- /dev/null +++ b/lib/crowdtilt/campaign.rb @@ -0,0 +1,48 @@ +module Crowdtilt + class Campaign < Model + include Crowdtilt::Model::Finders + uri_prefix '/campaigns' + + attr_accessor :admin, :creation_date, :description, :expiration_date, :fixed_payment_amount, :first_contributor, + :id, :img, :is_tilted, :is_paid, :is_expired, :metadata, :min_payment_amount, + :modification_date, :payments_uri, :privacy, :settlements_uri, :stats, :tax_id, + :tax_name, :tilter, :tilt_amount, :title, :type, :user_id, :uri + + coerce :admin => 'Crowdtilt::User' + coerce :tilter => 'Crowdtilt::User' + + def create_json + { "campaign" => { "user_id" => user_id, + "title" => title, + "description" => description, + "expiration_date" => expiration_date, + "tilt_amount" => tilt_amount } } + end + alias_method :update_json, :create_json + + def payments + raise "Can't verify a user without an ID" unless id + Crowdtilt::PaymentsArray.new self, Crowdtilt.get("/campaigns/#{id}/payments").body['payments'].map{|h| Crowdtilt::Payment.new(h)} + end + end + + class UserCampaignsArray < Array + attr_reader :user + def initialize(user, campaigns) + super campaigns + @user = user + end + + def find(id) + Crowdtilt::Campaign.new Crowdtilt.get("/user/#{user.id}/campaigns/"+id).body['campaign'] + end + + def build(params) + Crowdtilt::Campaign.new params.merge(:user => user.as_json) + end + + def create(params) + build(params).save + end + end +end \ No newline at end of file diff --git a/lib/crowdtilt/card.rb b/lib/crowdtilt/card.rb new file mode 100644 index 0000000..245ea18 --- /dev/null +++ b/lib/crowdtilt/card.rb @@ -0,0 +1,42 @@ +module Crowdtilt + class Card < Model + attr_accessor :expiration_month, :expiration_year, :number, + :security_code, :card_type, :creation_date, :id, + :last_four, :metadata, :user, :uri + + coerce :user => 'Crowdtilt::User' + + uri_prefix '/users/#{user.id}/cards' + + def create_json + { "card" => { "expiration_month" => expiration_month, + "expiration_year" => expiration_year, + "number" => number, + "security_code" => security_code } } + end + + def update_json + { "card" => { "metadata" => metadata } } + end + end + + class CardsArray < Array + attr_reader :user + def initialize(user, cards) + super cards + @user = user + end + + def find(id) + Crowdtilt::Card.new Crowdtilt.get("/users/#{user.id}/cards/"+id).body['card'].merge(:user => user) + end + + def build(params) + Crowdtilt::Card.new params.merge(:user => user.as_json) + end + + def create(params) + build(params).save + end + end +end \ No newline at end of file diff --git a/lib/crowdtilt/config.rb b/lib/crowdtilt/config.rb new file mode 100644 index 0000000..f8cb96c --- /dev/null +++ b/lib/crowdtilt/config.rb @@ -0,0 +1,34 @@ +module Crowdtilt + class Config + def initialize(&block) + instance_eval(&block) if block + end + + def key(val=nil) + @key ||= val + @key + end + + def secret(val=nil) + @secret ||= val + @secret + end + + def env(val=nil) + if val and not ['development','production'].include? val + raise "Unknown env '#{env}'" + end + @env ||= (val || "development") + @env + end + + def url + case env + when "development" + 'https://api-sandbox.crowdtilt.com' + when "production" + 'https://api.crowdtilt.com' + end + end + end +end \ No newline at end of file diff --git a/lib/crowdtilt/model.rb b/lib/crowdtilt/model.rb new file mode 100644 index 0000000..bd5082f --- /dev/null +++ b/lib/crowdtilt/model.rb @@ -0,0 +1,100 @@ +module Crowdtilt + class Model + include ActiveModel::Validations + include ActiveModel::Serialization + + def initialize(attributes) + deserialize attributes + end + + def persisted? + !!id + end + + # @todo remove me + def self.property(p,opts={}) + attr_accessor *p + end + + class << self + def uri_prefix(val=nil) + @uri_prefix = val if val + @uri_prefix + end + + def coerce(hash) + coercions.merge! hash + end + + def coercions + @coercions ||= {} + @coercions + end + end + + def save + raise "uri_prefix not specified for #{self.class}" unless uri_prefix + if id + Crowdtilt.put("#{uri_prefix}/#{id}", self.update_json) + else + deserialize Crowdtilt.post(uri_prefix, self.create_json).body[model_key] + end + true + # rescue Exception => e + # errors.add :general, e.message + # false + end + + def delete + Crowdtilt.delete("#{uri_prefix}/#{id}") + end + + module Finders + extend ActiveSupport::Concern + included do + class << self + def all + Crowdtilt.get(uri_prefix).body[plural_model_key].map{|h| self.new(h)} + end + + def find(id) + self.new Crowdtilt.get("#{uri_prefix}/#{id}").body[model_key] + end + end + end + end + + protected + + def deserialize(attributes) + attributes ||= {} + attributes.keys.each do |key| + if coercion = self.class.coercions[key.to_sym] + _klass = eval(coercion) + val = attributes[key].kind_of?(_klass) ? attributes[key] : _klass.new(attributes[key]) + self.send :"#{key}=", val + else + self.send :"#{key}=", attributes[key] + end + end + end + + def uri_prefix + eval("\"" + self.class.uri_prefix + "\"") + end + + def model_key + self.class.model_key + end + + class << self + def model_key + model_name.split("::").last.downcase + end + + def plural_model_key + [model_key,"s"].join + end + end + end +end \ No newline at end of file diff --git a/lib/crowdtilt/payment.rb b/lib/crowdtilt/payment.rb new file mode 100644 index 0000000..ded9e78 --- /dev/null +++ b/lib/crowdtilt/payment.rb @@ -0,0 +1,46 @@ +module Crowdtilt + class Payment < Model + uri_prefix '/campaigns/#{campaign.id}/payments' + + attr_accessor :amount, :user_fee_amount, :admin_fee_amount, :user_id, :card_id + + attr_accessor :status, :modification_date, :metadata, :id, :uri, :creation_date, + :campaign, :campaign_id, :card, :user + + coerce :user => 'Crowdtilt::User' + coerce :campaign => 'Crowdtilt::Campaign' + coerce :card => 'Crowdtilt::Card' + + def create_json + { "payment" => { "amount" => amount, + "user_fee_amount" => user_fee_amount, + "admin_fee_amount" => admin_fee_amount, + "user_id" => user_id, + "card_id" => card_id } } + end + + def update_json + { "payment" => { "metadata" => metadata } } + end + end + + class PaymentsArray < Array + attr_reader :campaign + def initialize(campaign, cards) + super cards + @campaign = campaign + end + + def find(id) + Crowdtilt::Payment.new Crowdtilt.get("/campaigns/#{campaign_id}/payments/"+id).body['payment'] + end + + def build(params) + Crowdtilt::Payment.new params.merge(:campaign => campaign) + end + + def create(params) + build(params).save + end + end +end \ No newline at end of file diff --git a/lib/crowdtilt/test.rb b/lib/crowdtilt/test.rb new file mode 100644 index 0000000..088d56a --- /dev/null +++ b/lib/crowdtilt/test.rb @@ -0,0 +1,26 @@ +require ::File.expand_path('../../../config/environment', __FILE__) + +user = Crowdtilt::User.new( + name: Faker::Name.name, + email: Faker::Internet.email + ).save + +v = Crowdtilt::Verification.new( + user: user, + dob: "1984-07", + phone_number: "(000) 000-0000", + street_address: "324 awesome address, awesome city, CA", + postal_code: "12345" + ) + +puts v.save +puts v.errors.inspect + +# card = user.cards.create expiration_year:2023, security_code:123, expiration_month:"01", number:"4111111111111111" + +# campaign = Crowdtilt::Campaign.all.last + +# payment = Crowdtilt::Payment.new(:user => user, :campaign => campaign, :card => card, :amount => 100, :user_fee_amount => 10, :admin_fee_amount => 10) +# payment.save + +# puts campaign.payments.inspect diff --git a/lib/crowdtilt/user.rb b/lib/crowdtilt/user.rb new file mode 100644 index 0000000..2f3feea --- /dev/null +++ b/lib/crowdtilt/user.rb @@ -0,0 +1,52 @@ +module Crowdtilt + class User < Model + include Crowdtilt::Model::Finders + uri_prefix '/users' + + attr_accessor :id, :name, :firstname, :lastname, :email, + :is_verified, :img, :creation_date, :last_login_date, + :uri, :campaigns_uri, :paid_campaigns_uri, :payments_uri, + :metadata + + def create_json + { "user" => { "firstname" => firstname, + "lastname" => lastname, + "email" => email } } + end + alias_method :update_json, :create_json + + def name=(name) + _a = name.split(' ') + @name = name + @firstname = _a[0] + @lastname = _a[1..-1].join(' ') + end + + def name + @name ||= [firstname,lastname].join(' ') + end + + def verified? + @is_verified.to_s == '1' + end + + def campaigns + raise "Can't verify a user without an ID" unless id + Crowdtilt::UserCampaignsArray.new self, Crowdtilt.get("/users/#{id}/campaigns").body['campaigns'].map{|h| Crowdtilt::Campaign.new(h)} + end + + def cards + raise "Can't load cards for a user without an ID" unless id + Crowdtilt::CardsArray.new self, Crowdtilt.get("/users/#{id}/cards").body['cards'].map{|h| Crowdtilt::Card.new(h)} + end + + def banks + raise "Can't load banks for a user without an ID" unless id + Crowdtilt::BanksArray.new self, Crowdtilt.get("/users/#{id}/banks").body['banks'].map{|h| Crowdtilt::Bank.new(h)} + end + + def payments + Crowdtilt.get("/users/#{id}/payments").body['payments'].map{|h| Crowdtilt::Payment.new(h)} + end + end +end \ No newline at end of file diff --git a/lib/crowdtilt/verification.rb b/lib/crowdtilt/verification.rb new file mode 100644 index 0000000..33de757 --- /dev/null +++ b/lib/crowdtilt/verification.rb @@ -0,0 +1,21 @@ +module Crowdtilt + class Verification < Model + attr_accessor :name, :dob, :phone_number, :street_address, :postal_code, :user + + def save + errors.add :user, "can not be nil" and return false if user.nil? + Crowdtilt.post("/users/#{user.id}/verification", + verification: { + name: user.name, + dob: dob, + phone_number: phone_number, + street_address: street_address, + postal_code: postal_code + }) + true + rescue Exception => e + errors.add :general, e.message + false + end + end +end \ No newline at end of file diff --git a/lib/crowdtilt/version.rb b/lib/crowdtilt/version.rb new file mode 100644 index 0000000..86b4de0 --- /dev/null +++ b/lib/crowdtilt/version.rb @@ -0,0 +1,3 @@ +module Crowdtilt + VERSION = '0.0.1' +end \ No newline at end of file diff --git a/spec/lib/crowdtilt/config_spec.rb b/spec/lib/crowdtilt/config_spec.rb new file mode 100644 index 0000000..de5d562 --- /dev/null +++ b/spec/lib/crowdtilt/config_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe "Configuring" do + it "should exception if configuration hasn't happened" do + expect { + Crowdtilt.config + }.to raise_error("Crowdtilt not initialize, please configure using Crowdtilt.configure") + end + + describe "defaults" do + context "env" do + it "should default to development env" do + Crowdtilt.configure + Crowdtilt.config.env.should == 'development' + end + end + + context "url" do + it "should be the sandbox url when the env is development" do + Crowdtilt.configure + Crowdtilt.config.url.should == 'https://api-sandbox.crowdtilt.com' + end + + it "should be the live url when the env is production" do + Crowdtilt.configure do + env "production" + end + Crowdtilt.config.url.should == 'https://api.crowdtilt.com' + end + end + end + + it "should set the values" do + Crowdtilt.configure do + key "abc123" + secret "foobar" + env "production" + end + Crowdtilt.config.key.should == "abc123" + Crowdtilt.config.secret.should == "foobar" + Crowdtilt.config.env.should == "production" + end +end \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..02f0c8b --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,8 @@ +require 'rubygems' +require 'bundler' + +$:.push(File.expand_path(File.dirname(__FILE__))) +require './lib/crowdtilt' + +Bundler.setup :default, :test +Bundler.require :default, :test \ No newline at end of file