Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

first commit

  • Loading branch information...
commit 5017669d4043eea2600bfa1e1f7d5cf7101d1d57 0 parents
Anders Törnqvist authored
Showing with 3,465 additions and 0 deletions.
  1. +8 −0 .gitignore
  2. +3 −0  .metrics
  3. +4 −0 Gemfile
  4. +112 −0 Gemfile.lock
  5. +21 −0 Rakefile
  6. +161 −0 example/chargify.rb
  7. +90 −0 example/dns_simple.rb
  8. +18 −0 example/google.rb
  9. +50 −0 example/google_shortener.rb
  10. +37 −0 example/google_translate.rb
  11. +110 −0 lib/blankslate.rb
  12. +189 −0 lib/resto.rb
  13. +56 −0 lib/resto/attributes.rb
  14. +34 −0 lib/resto/extra/copy.rb
  15. +26 −0 lib/resto/extra/delegation.rb
  16. +56 −0 lib/resto/extra/hash_args.rb
  17. +20 −0 lib/resto/format.rb
  18. +11 −0 lib/resto/format/default.rb
  19. +30 −0 lib/resto/format/json.rb
  20. +14 −0 lib/resto/format/plain.rb
  21. +25 −0 lib/resto/format/xml.rb
  22. +49 −0 lib/resto/property.rb
  23. +46 −0 lib/resto/property/handler.rb
  24. +29 −0 lib/resto/property/integer.rb
  25. +19 −0 lib/resto/property/string.rb
  26. +74 −0 lib/resto/request/base.rb
  27. +66 −0 lib/resto/request/factory.rb
  28. +53 −0 lib/resto/request/header.rb
  29. +126 −0 lib/resto/request/option.rb
  30. +50 −0 lib/resto/request/uri.rb
  31. +69 −0 lib/resto/response/base.rb
  32. +44 −0 lib/resto/translator/request_factory.rb
  33. +28 −0 lib/resto/translator/response_factory.rb
  34. +37 −0 lib/resto/validate.rb
  35. +39 −0 lib/resto/validate/inclusion.rb
  36. +36 −0 lib/resto/validate/length.rb
  37. +24 −0 lib/resto/validate/presence.rb
  38. +5 −0 lib/resto/version.rb
  39. +8 −0 readme.markdown
  40. +40 −0 resto.gemspec
  41. +58 −0 spec/resto/extra/copy_spec.rb
  42. +71 −0 spec/resto/extra/hash_args_spec.rb
  43. +24 −0 spec/resto/format/default_spec.rb
  44. +29 −0 spec/resto/format/json_spec.rb
  45. +21 −0 spec/resto/format/plain_spec.rb
  46. +13 −0 spec/resto/format/xml_spec.rb
  47. +57 −0 spec/resto/property/handler_spec.rb
  48. +67 −0 spec/resto/property/integer_spec.rb
  49. +60 −0 spec/resto/property_spec.rb
  50. +253 −0 spec/resto/request/base_spec.rb
  51. +114 −0 spec/resto/request/factory_spec.rb
  52. +93 −0 spec/resto/translator/response_factory_spec.rb
  53. +102 −0 spec/resto/validate/presence_spec.rb
  54. +531 −0 spec/resto_spec.rb
  55. +51 −0 spec/spec_helper.rb
  56. +4 −0 todo.txt
8 .gitignore
@@ -0,0 +1,8 @@
+pkg/*
+*.gem
+.bundle
+.rvmrc
+coverage
+tmp
+.DS_Store
+example/key_setup.rb
3  .metrics
@@ -0,0 +1,3 @@
+MetricFu::Configuration.run do |config|
+ config.metrics -= [:rcov]
+end
4 Gemfile
@@ -0,0 +1,4 @@
+source :gemcutter
+
+# Specify your gem's dependencies in resto.gemspec
+gemspec
112 Gemfile.lock
@@ -0,0 +1,112 @@
+PATH
+ remote: .
+ specs:
+ resto (0.0.1)
+ yajl-ruby (= 0.7.8)
+
+GEM
+ remote: http://rubygems.org/
+ specs:
+ Saikuro (1.1.0)
+ abstract (1.0.0)
+ activesupport (3.0.4)
+ addressable (2.2.2)
+ arrayfields (4.7.4)
+ chronic (0.2.3)
+ hoe (>= 1.2.1)
+ churn (0.0.13)
+ chronic (>= 0.2.3)
+ hirb
+ json_pure
+ main
+ ruby_parser (~> 2.0.4)
+ sexp_processor (~> 3.0.3)
+ code-cleaner (0.8.2)
+ colored (1.2)
+ crack (0.1.8)
+ diff-lcs (1.1.2)
+ erubis (2.6.6)
+ abstract (>= 1.0.0)
+ fattr (2.2.0)
+ flay (1.4.1)
+ ruby_parser (~> 2.0)
+ sexp_processor (~> 3.0)
+ flog (2.5.0)
+ ruby_parser (~> 2.0)
+ sexp_processor (~> 3.0)
+ haml (3.0.25)
+ hirb (0.3.6)
+ hoe (2.9.1)
+ rake (>= 0.8.7)
+ i18n (0.5.0)
+ json_pure (1.5.1)
+ main (4.4.0)
+ arrayfields (>= 4.7.4)
+ fattr (>= 2.1.0)
+ metric_fu (2.0.1)
+ Saikuro (>= 1.1.0)
+ activesupport (>= 2.0.0)
+ chronic (~> 0.2.3)
+ churn (>= 0.0.7)
+ flay (>= 1.2.1)
+ flog (>= 2.2.0)
+ rails_best_practices (>= 0.3.16)
+ rcov (>= 0.8.3.3)
+ reek (>= 1.2.6)
+ roodi (>= 2.1.0)
+ metrical (0.0.4)
+ activesupport
+ metric_fu (~> 2.0.1)
+ rails_best_practices (0.7.0)
+ activesupport
+ colored (~> 1.2)
+ erubis (~> 2.6.6)
+ haml (~> 3.0.18)
+ i18n
+ ruby-progressbar (~> 0.0.9)
+ ruby_parser (~> 2.0.4)
+ rake (0.8.7)
+ rcov (0.9.9)
+ reek (1.2.8)
+ ruby2ruby (~> 1.2)
+ ruby_parser (~> 2.0)
+ sexp_processor (~> 3.0)
+ roodi (2.1.0)
+ ruby_parser
+ rspec (2.0.1)
+ rspec-core (~> 2.0.1)
+ rspec-expectations (~> 2.0.1)
+ rspec-mocks (~> 2.0.1)
+ rspec-core (2.0.1)
+ rspec-expectations (2.0.1)
+ diff-lcs (>= 1.1.2)
+ rspec-mocks (2.0.1)
+ rspec-core (~> 2.0.1)
+ rspec-expectations (~> 2.0.1)
+ ruby-progressbar (0.0.9)
+ ruby2ruby (1.2.5)
+ ruby_parser (~> 2.0)
+ sexp_processor (~> 3.0)
+ ruby_parser (2.0.5)
+ sexp_processor (~> 3.0)
+ sexp_processor (3.0.5)
+ simplecov (0.4.0)
+ simplecov-html (~> 0.4.0)
+ simplecov-html (0.4.3)
+ webmock (1.3.5)
+ addressable (>= 2.1.1)
+ crack (>= 0.1.7)
+ yajl-ruby (0.7.8)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ bundler (>= 1.0.0)
+ code-cleaner (= 0.8.2)
+ metrical
+ reek
+ resto!
+ rspec (>= 2.0.1)
+ simplecov
+ webmock (= 1.3.5)
21 Rakefile
@@ -0,0 +1,21 @@
+require 'bundler'
+Bundler::GemHelper.install_tasks
+
+$LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
+
+require "resto/version"
+
+desc "open console (require 'resto')"
+task :c do
+ system "irb -I lib -r resto"
+end
+
+desc "adds encoding utf-8.."
+task :clean do
+ system "code-cleaner . --encoding=utf-8"
+end
+
+desc "Runs all the specs."
+task :spec => :clean do
+ system "bundle exec rspec spec"
+end
161 example/chargify.rb
@@ -0,0 +1,161 @@
+# encoding: utf-8
+$LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
+require File.expand_path(File.join(File.dirname(__FILE__), 'key_setup.rb'))
+
+require 'resto'
+require 'pp'
+
+# http://docs.chargify.com/api-authentication
+# http://docs.chargify.com/api-introduction
+# http://docs.chargify.com/api-resources
+class Chargify
+ include Resto
+
+ resto_request do
+ format :json, :extension => true
+ basic_auth(:username => API_KEY, :password => API_PASSWORD)
+ host 'https://dns-parrot.chargify.com/'
+ end
+
+ resto_response do
+ format :json
+ end
+end
+
+# http://docs.chargify.com/api-products
+class Product < Chargify
+ resource_identifier :id
+ property :id, Integer
+ property :price_in_cents, Integer
+ property :interval, Integer
+ property :created_at, String
+ property :name, String
+
+ resto_request do
+ path 'products'
+ translator [:product]
+ end
+
+ resto_response do
+ translator [:product]
+ end
+
+end
+
+#puts "products ************************"
+#products = Product.all
+#puts products.size
+#puts products.first.attributes
+#product = products.first
+##
+#puts "product.get(#{product.id}) ***************"
+#product = Product.get(product.id)
+#puts product.attributes
+
+#puts "\n\n\n"
+
+class Customer < Chargify
+
+ # The id is the unique identifier for this customer within Chargify
+ resource_identifier :id
+ property :id, Integer #{ validate_presence }
+ property :first_name, String #{ validate_presence }
+ property :last_name, String #{ validate_presence }
+ property :email, String #{ validate_presence }
+ property :organization, String
+ property :created_at, String
+ property :updated_at, String
+
+ resto_request do
+ path 'customers'
+ translator [:customer]
+ end
+
+ resto_response do
+ translator [:customer]
+ end
+end
+
+body = {:first_name =>"Anders ska bort",
+ :last_name => "Dum användare",
+ :email => "anders.tornqvist@elabs.se" }
+
+#puts "Customer.post(body)"
+#customer = Customer.post(body)
+#puts customer.attributes
+#puts "\n"
+
+#Customer deletion is not currently supported
+#(you will receive a 403 Forbidden response
+#puts "Customer.delete(customer.id)"
+#removed = Customer.delete(customer.id)
+#puts removed.inspect
+#puts removed.code
+#puts removed.body
+
+
+#puts "customers **************"
+#customers = Customer.all
+#puts customers.size
+#customers.each { |customer| puts customer.attributes; puts "\n" }
+#customer = customers.first
+##
+#puts "Customer.get(#{customer.id}) *************"
+#customer = Customer.get(customer.id)
+#puts customer.attributes
+
+#puts "\n\n"
+
+
+# http://docs.chargify.com/api-subscriptions
+# curl -u <api_key>:x -H Accept:application/json -H
+# Content-Type:application/json https://acme.chargify.com/subscriptions.json
+class Subscription < Chargify
+
+ resource_identifier :id
+ property :id, Integer
+ property :product_id, Integer
+ property :customer_id, Integer
+ property :cancellation_message, String
+ property :state, String
+
+ resto_request do
+ path '/subscriptions'
+ translator [:subscription]
+ end
+
+ resto_response do
+ translator [:subscription]
+ end
+
+end
+
+attributes = {
+ :product_id => 25450, # product = { id: 25450, name: 'dns' }
+ :customer_id => 423360, # customer = { id: 416942, email: joe@example.com }
+}
+
+puts "subscriptions *************** \n"
+subscriptions = Subscription.all
+puts subscriptions.size
+#puts subscriptions.first.response.body
+
+subscriptions[1].tap {|s| puts "id: #{s.object_id}" }.get
+ .tap { |s| puts "id: #{s.object_id}" }.reload
+ .tap { |s| puts "id: #{s.object_id}, state: #{s.cancellation_message}" }
+ .body(:cancellation_message => 'just a test').put
+ .tap { |s| puts "id: #{s.object_id}, state: #{s.cancellation_message}" }
+ .update_attributes(:cancellation_message => 'updated again').put
+ .tap { |s| puts "id: #{s.object_id}, state: #{s.cancellation_message}" }
+
+
+
+#subscription = Subscription.post attributes
+#puts subscription.attributes
+#subscription = subscriptions.each do |s|
+# puts s.attributes
+#end
+
+#subscription = Subscription.delete(415520)
+#puts subscription.response.response
+#puts subscription.attributes
90 example/dns_simple.rb
@@ -0,0 +1,90 @@
+# encoding: utf-8
+$LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
+require File.expand_path(File.join(File.dirname(__FILE__), 'key_setup.rb'))
+
+require 'resto'
+require 'pp'
+
+class DomainTranslator
+ def call(klass, hash)
+ klass.new(hash['data']['translations'][0])
+ end
+end
+
+class Domain
+ include Resto
+
+ property :created_at, String #2010-12-04T22:10:57Z
+ property :expires_at, String
+ property :id, String
+ property :name, String
+ property :name_server_status, String
+ property :registrant_id, String
+ property :registration_status, String
+ property :updated_at, String #2010-12-04T22:10:57Z
+ property :user_id, String
+ property :uses_external_name_servers, String
+
+ resto_request do
+ format :json, :extension => :true
+ basic_auth(:username => USERNAME,
+ :password => PASSWORD)
+ #host 'https://dnsimple.com/'
+ host 'https://test.dnsimple.com/'
+ path '/domains'
+ #ca_file File.join('/Users/unders/CA', "server.crt")
+ cert OpenSSL::X509::Certificate.new(File.read("/Users/unders/CA/server.crt"))
+ read_timeout 5
+ #http.verify_depth = 5
+ verify_none
+ #verify_callback(
+ # Proc.new do |preverify_ok, ssl_context|
+ #
+ # puts "inne i callback"
+ # if preverify_ok != true || ssl_context.error != 0
+ # puts ssl_context.inspect
+ # err_msg = "SSL Verification failed -- Preverify: #{preverify_ok}, Error: #{ssl_context.error_string} (#{ssl_context.error})"
+ # raise OpenSSL::SSL::SSLError.new(err_msg)
+ # true
+ # end
+ # true
+ # end
+ # )
+ # body_translator [:domain]
+ end
+
+ resto_response do
+ format :json
+ translator [:domain]
+ end
+end
+
+collection = Domain.all
+
+puts collection.length
+
+#collection.each_with_index do |domain, index|
+# puts domain.id
+# puts domain.name
+# puts domain.valid?
+# puts domain.registrant_id
+# pp domain.response.read_body[index]
+# pp domain.response.response.code
+# pp domain.response.response.body
+#end
+
+# I get an error at:
+# /Users/unders/.rvm/rubies/ruby-1.9.2-p0/lib/ruby/1.9.1/net/protocol.rb:146:in
+# `rescue in rbuf_fill': Timeout::Error (Timeout::Error)
+
+dns_parrot = Domain.post(:domain => { :name => 'dnsddd-parrot.se'})
+
+pp dns_parrot
+pp dns_parrot.response.read_body
+pp dns_parrot.response.response.code
+pp dns_parrot.response.response.body
+
+
+puts dns_parrot.name
+puts dns_parrot.valid?
+
18 example/google.rb
@@ -0,0 +1,18 @@
+# encoding: utf-8
+
+$LOAD_PATH << File.dirname(__FILE__) + "../../lib"
+require 'resto'
+
+msg = "ddddddddd"
+# Resto.url('www.google.com').set_debug_output($stderr).get
+# Resto.url('www.google.com').set_debug_output($stdout).get
+# Resto.url('www.google.com').set_debug_output(msg).get
+
+ # $stderr << msg
+ $stderr << "\n"
+
+# puts msg.inspect
+
+puts Resto.url('https://google.com').set_debug_output($stderr).verify_none.get
+# puts Resto.url('https://encrypted.google.com/')
+# .set_debug_output($stderr).verify_peer.get
50 example/google_shortener.rb
@@ -0,0 +1,50 @@
+# encoding: utf-8
+$LOAD_PATH << File.dirname(__FILE__) + "../../lib"
+require File.expand_path(File.join(File.dirname(__FILE__), 'key_setup.rb'))
+require 'resto'
+
+# http://goo.gl/
+# http://code.google.com/apis/urlshortener/v1/getting_started.html
+class Goog
+ include Resto
+
+ property :id, String
+ property :kind, String
+ property :long_url, String, :remote_name => 'longUrl'
+ property :status, String
+ # property :analytics Hash
+ # property :analytics Analytic
+
+ resto_request do
+ format :json
+ host 'https://www.googleapis.com'
+ path '/urlshortener/v1/url'
+ query "key=#{GOOGLE_KEY}"
+ end
+
+ resto_response do
+ format :json
+ translator :default
+ end
+end
+
+# g = Goog.fetch(:shortUrl => "http://goo.gl/fbsSx") # the added 'x'
+# att the end makes the request invalid
+g = Goog.fetch(:shortUrl => "http://goo.gl/fbsS")
+puts "id: #{g.id}\n" + "kind: #{g.kind}\n" +
+ "long_url: #{g.long_url}\n" + "status: #{g.status}\n"
+puts "valid: #{g.valid?}"
+puts g.response
+puts g.response.code
+#puts g.response.message
+
+g = Goog.post(:longUrl => 'http://svt.se/2.22620/1.2309372/om_signalspaning')
+puts "valid?: #{g.valid?}"
+puts g.response.body
+puts g.response.read_body
+puts g.id
+
+# g = Goog.fetch(:shortUrl => g.id, :projection => 'FULL')
+# g = Goog.fetch(:shortUrl => g.id, :projection => 'ANALYTICS_TOP_STRINGS')
+g = Goog.fetch(:shortUrl => g.id, :projection => 'ANALYTICS_CLICKS')
+puts g.response.body
37 example/google_translate.rb
@@ -0,0 +1,37 @@
+# encoding: utf-8
+$LOAD_PATH << File.dirname(__FILE__) + "../../lib"
+require File.expand_path(File.join(File.dirname(__FILE__), 'key_setup.rb'))
+
+require 'resto'
+
+# http://code.google.com/apis/language/translate/v2/using_rest.html
+# https://github.com/jimmycuadra/to_lang <- Google translate
+
+class GoogTranslator
+ def call(klass, hash)
+ klass.new(hash['data']['translations'][0])
+ end
+end
+
+class Goog
+ include Resto
+
+ property :translated_text, String, :remote_name => 'translatedText'
+
+ resto_request do
+ format :json
+ host 'https://www.googleapis.com'
+ path '/language/translate/v2'
+ query "key=#{GOOGLE_KEY}"
+ end
+
+ resto_response do
+ format :json
+ # translator GoogTranslator
+ # translator lambda { |klass, hash| klass.new(hash['data']['translations'][0]) }
+ translator lambda { |_, hash| Goog.new(hash['data']['translations'][0]) }
+ end
+end
+
+g = Goog.fetch(:source => "sv", :target => 'en', :q => "Hej världen!")
+puts g.translated_text
110 lib/blankslate.rb
@@ -0,0 +1,110 @@
+# encoding: utf-8
+#!/usr/bin/env ruby
+#--
+# Copyright 2004, 2006 by Jim Weirich (jim@weirichhouse.org).
+# All rights reserved.
+
+# Permission is granted for use, copying, modification, distribution,
+# and distribution of modified versions of this work as long as the
+# above copyright notice is included.
+#++
+
+######################################################################
+# BlankSlate provides an abstract base class with no predefined
+# methods (except for <tt>\_\_send__</tt> and <tt>\_\_id__</tt>).
+# BlankSlate is useful as a base class when writing classes that
+# depend upon <tt>method_missing</tt> (e.g. dynamic proxies).
+#
+class BlankSlate
+ class << self
+
+ # Hide the method named +name+ in the BlankSlate class. Don't
+ # hide +instance_eval+ or any method beginning with "__".
+ def hide(name)
+ if instance_methods.include?(name.to_s) and
+ name !~ /^(__|instance_eval)/
+ @hidden_methods ||= {}
+ @hidden_methods[name.to_sym] = instance_method(name)
+ undef_method name
+ end
+ end
+
+ def find_hidden_method(name)
+ @hidden_methods ||= {}
+ @hidden_methods[name] || superclass.find_hidden_method(name)
+ end
+
+ # Redefine a previously hidden method so that it may be called on a blank
+ # slate object.
+ def reveal(name)
+ hidden_method = find_hidden_method(name)
+ fail "Don't know how to reveal method '#{name}'" unless hidden_method
+ define_method(name, hidden_method)
+ end
+ end
+
+ instance_methods.each { |m| hide(m) }
+end
+
+######################################################################
+# Since Ruby is very dynamic, methods added to the ancestors of
+# BlankSlate <em>after BlankSlate is defined</em> will show up in the
+# list of available BlankSlate methods. We handle this by defining a
+# hook in the Object and Kernel classes that will hide any method
+# defined after BlankSlate has been loaded.
+#
+module Kernel
+ class << self
+ alias_method :blank_slate_method_added, :method_added
+
+ # Detect method additions to Kernel and remove them in the
+ # BlankSlate class.
+ def method_added(name)
+ result = blank_slate_method_added(name)
+ return result if self != Kernel
+ BlankSlate.hide(name)
+ result
+ end
+ end
+end
+
+######################################################################
+# Same as above, except in Object.
+#
+class Object
+ class << self
+ alias_method :blank_slate_method_added, :method_added
+
+ # Detect method additions to Object and remove them in the
+ # BlankSlate class.
+ def method_added(name)
+ result = blank_slate_method_added(name)
+ return result if self != Object
+ BlankSlate.hide(name)
+ result
+ end
+
+ def find_hidden_method(name)
+ nil
+ end
+ end
+end
+
+######################################################################
+# Also, modules included into Object need to be scanned and have their
+# instance methods removed from blank slate. In theory, modules
+# included into Kernel would have to be removed as well, but a
+# "feature" of Ruby prevents late includes into modules from being
+# exposed in the first place.
+#
+class Module
+ alias blankslate_original_append_features append_features
+ def append_features(mod)
+ result = blankslate_original_append_features(mod)
+ return result if mod != Object
+ instance_methods.each do |name|
+ BlankSlate.hide(name)
+ end
+ result
+ end
+end
189 lib/resto.rb
@@ -0,0 +1,189 @@
+# encoding: utf-8
+
+require 'resto/request/base'
+require 'resto/response/base'
+require 'resto/property'
+require 'resto/attributes'
+require 'resto/extra/copy'
+
+module Resto
+ def self.included(klass)
+ klass.extend ClassMethods
+ klass.class_eval do
+ @request = Request::Base.new
+ @response = Response::Base.new.klass(klass)
+ end
+ end
+
+ def self.url(url)
+ Request::Base.new.url(url)
+ end
+end
+
+module Resto
+ module ClassMethods
+
+ def inherited(sub_class)
+ sub_class.class_exec(self) do |parent_class|
+ @request = parent_class.request
+ @response = parent_class.base_response.klass(sub_class)
+ end
+ end
+
+ def resto_request(&block)
+ @request.instance_exec(&block)
+ end
+
+
+ def resto_response(&block)
+ @response.instance_exec(&block)
+ end
+
+ def resource_id
+ @resource_identifier
+ end
+
+ def resource_identifier(id)
+ @resource_identifier = id
+ end
+
+ def property(name, property, options={}, &block)
+ property = Resto::Property.const_get(property.to_s).new(name, options)
+ property.instance_exec(&block) if block_given?
+
+ property_handler.add(property)
+
+ attribute_methods = %Q{
+ def #{name}
+ @attributes.get(:#{name})
+ end
+
+ def #{name}_without_cast
+ @attributes.get_without_cast(:#{name})
+ end
+
+ def #{name}?
+ @attributes.present?(:#{name})
+ end
+
+ def #{name}=(value)
+ @attributes.set(:#{name}, value)
+ end
+ }
+
+ class_eval(attribute_methods, __FILE__, __LINE__)
+ end
+
+
+ def all(params = {})
+ res =
+ if params.keys.empty?
+ request.get
+ else
+ request.params(params).get
+ end
+
+ response(res).all
+ end
+
+ def head
+ response(request.head)
+ end
+
+ def get(id)
+ response(request.append_path(id).get).get
+ end
+
+ def fetch(params = {})
+ res =
+ if params.keys.empty?
+ request.get
+ else
+ request.params(params).get
+ end
+
+ response(res).get
+ end
+
+ def post(attributes)
+ attributes.delete(resource_id)
+ response(request.body(attributes).post).get
+ end
+
+ def put(attributes)
+ id = attributes.delete(resource_id)
+ response(request.append_path(id).body(attributes).put).get
+ end
+
+ def delete(id)
+ response(request.append_path(id).delete).get
+ end
+
+ def request
+ Extra::Copy.request_base(@request)
+ end
+
+ def response(response)
+ base_response.http_response(response)
+ end
+
+ def base_response
+ Extra::Copy.response_base(@response)
+ end
+
+ def property_handler
+ @property_handler ||= Property::Handler.new
+ end
+
+ end
+end
+
+module Resto
+ def initialize(attributes)
+ raise "Must be a hash" unless attributes.is_a?(Hash)
+
+ @attributes = Attributes.new(attributes, self)
+ end
+
+ attr_accessor :response
+
+ def get
+ id = attributes.fetch(self.class.resource_id)
+ self.class.get(id)
+ end
+
+ alias reload get
+
+ def put
+ self.class.put(attributes)
+ end
+
+ def delete
+ id = attributes.fetch(self.class.resource_id)
+ self.class.delete(id)
+ end
+
+ def valid?
+ valid_response = response ? (/\A20\d{1}\z/ =~ response.code) == 0 : true
+ @attributes.valid? and valid_response
+ end
+
+ def add_error(key, value)
+ @attributes.add_error(key, value)
+ end
+
+ def errors
+ @attributes.errors
+ end
+
+ def update_attributes(attributes)
+ tap { @attributes.update_attributes(attributes) }
+ end
+
+ alias body update_attributes
+
+ def attributes
+ @attributes.to_hash
+ end
+
+end
56 lib/resto/attributes.rb
@@ -0,0 +1,56 @@
+# encoding: utf-8
+
+module Resto
+ class Attributes
+ def initialize(attributes, resource, property_handler = nil)
+ @resource = resource
+ @property_handler = property_handler || resource.class.property_handler
+ @attributes = {} # TODO must handle indifferent access :name and 'name'
+ @attributes_before_cast = {}
+ @errors = {}
+
+ update_attributes(attributes)
+ end
+
+ def update_attributes(attributes)
+ attributes.each do |key, value|
+ key = @property_handler.attribute_key(key)
+ set(key, value) if key
+ end
+ end
+
+ def set(key, value)
+ @attributes_before_cast.store(key, value)
+ @attributes.store(key, @property_handler.cast(key, value, @errors))
+ end
+
+ def get(key)
+ @attributes.fetch(key, nil)
+ end
+
+ def get_without_cast(key)
+ @attributes_before_cast.fetch(key, nil)
+ end
+
+ def present?(key)
+ @attributes.fetch(key, false) ? true : false
+ end
+
+ def valid?
+ @property_handler.validate(@resource)
+ errors.empty?
+ end
+
+ def add_error(key, value)
+ @errors.store(key, value)
+ end
+
+ def errors
+ @errors.map {|key, value| value }.compact
+ end
+
+ def to_hash
+ @attributes
+ end
+ end
+end
34 lib/resto/extra/copy.rb
@@ -0,0 +1,34 @@
+# encoding: utf-8
+
+require 'resto/request/base'
+require 'resto/response/base'
+
+module Resto
+ module Extra
+ module Copy
+
+ def self.request_base(request_base)
+ Resto::Request::Base.new.tap do |copy|
+ copy_instance_variables(request_base, copy, ["@request"])
+
+ request_klass = request_base.instance_variable_get("@request_klass")
+ copy.instance_variable_set("@request", request_klass.new(copy))
+ end
+ end
+
+ def self.response_base(response_base)
+ Resto::Response::Base.new.tap do |copy|
+ copy_instance_variables(response_base, copy, ["@response"])
+ end
+ end
+
+ def self.copy_instance_variables(from, to, exclude = [])
+ (from.instance_variables.map(&:to_s) - exclude).each do |name|
+ instance_variable = from.instance_variable_get(name)
+
+ to.instance_variable_set(name, instance_variable)
+ end
+ end
+ end
+ end
+end
26 lib/resto/extra/delegation.rb
@@ -0,0 +1,26 @@
+# encoding: utf-8
+
+module Resto
+ module Extra
+ module Delegation
+
+ def delegate(*methods)
+ options = methods.pop
+ to = options[:to]
+ unless options.is_a?(Hash) && to
+ raise ArgumentError, "Delegation needs a target. Supply an options
+ hash with a :to key as the last argument
+ (e.g. delegate :hello, :to => :greeter)."
+ end
+
+ methods.each do |method|
+ class_eval <<-EOS
+ def #{method}(*args, &block)
+ #{to}.__send__(#{method.inspect}, *args, &block)
+ end
+ EOS
+ end
+ end
+ end
+ end
+end
56 lib/resto/extra/hash_args.rb
@@ -0,0 +1,56 @@
+# encoding: utf-8
+
+class Resto::Extra::HashArgs; end
+
+class << Resto::Extra::HashArgs
+
+ def key(key)
+ @keys ||= []
+
+ unless key.is_a?(Symbol)
+ raise ArgumentError, "The key '#{key}' must be a symbol"
+ end
+
+ if @keys.include?(key)
+ raise ArgumentError, "The key '#{key}' has already been defined."
+ end
+
+ @keys << key
+ end
+
+private
+ def assert_key(key)
+ unless @keys.include?(key.to_sym)
+ raise ArgumentError, "The key '#{key}' is not valid.
+ Valid keys are: #{@keys.join(' ,')}"
+ end
+ end
+end
+
+class Resto::Extra::HashArgs
+
+ def initialize(hash)
+ hash ||= {}
+ raise ArgumentError, "'#{hash}' must be a Hash" unless hash.is_a?(Hash)
+ keys = hash.keys
+ keys_as_symbols = keys.map(&:to_sym)
+ if (keys_as_symbols.uniq.size != keys_as_symbols.size)
+ raise ArgumentError, "duplicated keys: #{keys.join(', ')}"
+ end
+
+ @hash = {}
+ keys.each do |key|
+ self.class.send(:assert_key, key)
+ @hash[key.to_sym] = hash.fetch(key)
+ end
+ end
+
+ def fetch(key, &block)
+ self.class.send(:assert_key, key)
+ @hash.fetch(key, &block)
+ end
+
+ def keys
+ @hash.keys
+ end
+end
20 lib/resto/format.rb
@@ -0,0 +1,20 @@
+# encoding: utf-8
+
+require 'resto/format/default'
+require 'resto/format/plain'
+require 'resto/format/json'
+require 'resto/format/xml'
+
+module Resto
+ module Format
+ def self.get(symbol=:default)
+ format = Resto::Format.const_get("#{symbol.to_s.capitalize}")
+ end
+
+ def extension; end
+ def accept; '*/*'; end
+ def content_type; end
+ def encode(*args); args.first end
+ def decode(*args); args.first end
+ end
+end
11 lib/resto/format/default.rb
@@ -0,0 +1,11 @@
+# encoding: utf-8
+
+module Resto
+ module Format
+ class Default; end
+
+ class << Default
+ include Format
+ end
+ end
+end
30 lib/resto/format/json.rb
@@ -0,0 +1,30 @@
+# encoding: utf-8
+
+# http://en.wikipedia.org/wiki/JSON
+require 'yajl'
+
+module Resto
+ module Format
+ class Json; end
+
+ class << Json
+ include Format
+
+ def extension; 'json'; end
+ def accept; 'application/json, */*'; end
+ def content_type; 'application/json'; end
+
+ def encode(hash, options = nil)
+ raise ArgumentError unless hash.is_a?(Hash)
+
+ Yajl::Encoder.encode(hash)
+ end
+
+ def decode(json)
+ raise ArgumentError unless json.is_a?(String)
+
+ Yajl::Parser.parse(json)
+ end
+ end
+ end
+end
14 lib/resto/format/plain.rb
@@ -0,0 +1,14 @@
+# encoding: utf-8
+
+module Resto
+ module Format
+ class Plain; end
+
+ class << Plain
+ include Format
+
+ def accept; 'text/plain, */*'; end
+ def content_type; 'text/plain'; end
+ end
+ end
+end
25 lib/resto/format/xml.rb
@@ -0,0 +1,25 @@
+# encoding: utf-8
+
+# https://tools.ietf.org/html/rfc3023
+
+module Resto
+ module Format
+ class Xml; end
+
+ class << Xml
+ include Format
+
+ def extension; 'xml'; end
+ def accept; 'application/xml, */*'; end
+ def content_type; 'application/xml;charset=utf-8'; end
+
+ def encode(hash, options = nil)
+
+ end
+
+ def decode(json)
+
+ end
+ end
+ end
+end
49 lib/resto/property.rb
@@ -0,0 +1,49 @@
+# encoding: utf-8
+
+require 'resto/validate'
+require 'resto/property/handler'
+require 'resto/property/string'
+require 'resto/property/integer'
+
+module Resto
+ module Property
+
+ def initialize(name, options={})
+ raise ':name must be a symbol' unless name.is_a?(Symbol)
+
+ @name = name
+ @remote_name = options.fetch(:remote_name) { name }
+ @validations = []
+ end
+
+ def remote_key
+ @remote_name.to_s
+ end
+
+ def attribute_key
+ @name
+ end
+
+ def attribute_key_as_string
+ @name.to_s
+ end
+
+ def validate_presence
+ Validate::Presence.new.tap { |validation| @validations.push(validation) }
+ end
+
+ def validate_inclusion
+ Validate::Inclusion.new.tap { |validation| @validations.push(validation) }
+ end
+
+ def validate_length
+ Validate::Length.new.tap { |validation| @validations.push(validation) }
+ end
+
+ def validate(resource, attribute_key)
+ @validations.each do |validate|
+ validate.attribute_value(resource, attribute_key)
+ end
+ end
+ end
+end
46 lib/resto/property/handler.rb
@@ -0,0 +1,46 @@
+# encoding: utf-8
+module Resto
+ module Property
+ class Handler
+ def initialize
+ @properties = {} # TODO fix indifferent access
+ @properties_with_indifferent_access = {}
+ end
+
+ def add(property)
+ @properties_with_indifferent_access.store(property.remote_key, property)
+ @properties_with_indifferent_access
+ .store(property.attribute_key, property)
+ @properties_with_indifferent_access
+ .store(property.attribute_key_as_string, property)
+
+ @properties.store(property.attribute_key, property)
+ end
+
+ def attribute_key(key)
+ get(key, 'attribute_key')
+ end
+
+ def cast(key, value, errors)
+ get(key).cast(value, errors)
+ end
+
+ def validate(resource)
+ @properties.each do |key, property|
+ property.validate(resource, key)
+ end
+ end
+
+ private
+ def get(key, method=nil)
+ property = @properties_with_indifferent_access.fetch(key, false)
+
+ if (property and method)
+ property.send(method)
+ else
+ property
+ end
+ end
+ end
+ end
+end
29 lib/resto/property/integer.rb
@@ -0,0 +1,29 @@
+# encoding: utf-8
+
+module Resto
+ module Property
+ class Integer; end
+
+ class << Integer
+ end
+
+ class Integer
+ include Property
+
+ def initialize(name, options={})
+ @key = (name.to_s + "_integer").to_sym
+ super
+ end
+
+ def cast(value, errors)
+ errors.store(@key, nil)
+
+ begin
+ value.to_s.strip.empty? ? nil : Integer(value)
+ rescue ArgumentError, TypeError
+ nil.tap { errors.store(@key, ":#{attribute_key} is not an integer.") }
+ end
+ end
+ end
+ end
+end
19 lib/resto/property/string.rb
@@ -0,0 +1,19 @@
+# encoding: utf-8
+
+module Resto
+ module Property
+ class String; end
+
+ class << String
+ end
+
+ class String
+ include Property
+
+ def cast(value, errors=nil)
+ value.to_s
+ end
+ end
+
+ end
+end
74 lib/resto/request/base.rb
@@ -0,0 +1,74 @@
+# encoding: utf-8
+
+require 'resto/extra/delegation'
+require 'resto/request/uri'
+require 'resto/request/header'
+require 'resto/request/option'
+require 'resto/request/factory'
+require 'resto/translator/request_factory'
+
+module Resto
+ module Request
+ class Base
+ extend Resto::Extra::Delegation
+ include Resto::Request::Header
+ include Resto::Request::Uri
+ include Resto::Request::Option
+
+ delegate :head, :get, :post, :put, :delete, :to => :@request
+
+ def initialize(request=Resto::Request::Factory)
+ @request_klass = request
+ @request = @request_klass.new(self)
+ end
+
+ def url(url)
+ tap { parse_url(url) }
+ end
+
+ def host(host)
+ tap { @host = host }
+ end
+
+ def port(port)
+ tap { @port = port }
+ end
+
+ def path(path)
+ tap { @path = path }
+ end
+
+ def append_path(append_path)
+ tap { @append_path = append_path }
+ end
+
+ def query(query)
+ tap { @query = query }
+ end
+
+ def params(hash)
+ tap { @params = hash }
+ end
+
+ def body(body)
+ tap { @body = body }
+ end
+
+ def translator(translator)
+ @translator = Resto::Translator::RequestFactory.create(translator)
+ self
+ end
+
+ def read_body
+ if @body
+ body = @translator ? @translator.call(@body) : @body
+ current_formatter.encode(body)
+ end
+ end
+
+ def current_formatter
+ @formatter ||= Resto::Format.get(@symbol || :default)
+ end
+ end
+ end
+end
66 lib/resto/request/factory.rb
@@ -0,0 +1,66 @@
+# encoding: utf-8
+
+require 'net/https'
+require 'resto/extra/delegation'
+
+module Resto
+ module Request
+ class Factory
+ extend Resto::Extra::Delegation
+
+ delegate :read_host, :read_port, :options, :read_body, :composed_path,
+ :composed_headers, :scheme, :use_ssl, :to => :@request
+
+ def initialize(request)
+ @request = request
+ end
+
+ def head
+ http.start do |http|
+ http.request(Net::HTTP::Head.new(composed_path, composed_headers))
+ end
+ end
+
+ def get
+ http.start do |http|
+ http.request(Net::HTTP::Get.new(composed_path, composed_headers))
+ end
+ end
+
+ def post
+ http.start do |http|
+ http.request(Net::HTTP::Post.new(composed_path, composed_headers),
+ read_body)
+ end
+ end
+
+ def put
+ http.start do |http|
+ http.request(Net::HTTP::Put.new(composed_path, composed_headers),
+ read_body)
+ end
+ end
+
+ def delete
+ http.start do |http|
+ http.request(Net::HTTP::Delete.new(composed_path, composed_headers))
+ end
+ end
+
+ def http
+ ::Net::HTTP.new(read_host, read_port).tap do |http|
+
+ use_ssl if scheme == "https"
+
+ unless options.keys.empty?
+ http.methods.grep(/\A(\w+)=\z/) do |meth|
+ key = $1.to_sym
+ options.key?(key) or next
+ http.__send__(meth, options[key])
+ end
+ end
+ end
+ end
+ end
+ end
+end
53 lib/resto/request/header.rb
@@ -0,0 +1,53 @@
+# encoding: utf-8
+require 'resto/format'
+
+require 'resto/extra/hash_args'
+class BasicAuth < Resto::Extra::HashArgs
+ key :username
+ key :password
+end
+
+class FormatExtension < Resto::Extra::HashArgs
+ key :extension
+end
+
+module Resto
+ module Request
+ module Header
+
+ def format(symbol, options=nil)
+ formatter(Resto::Format.get(@symbol = symbol), options)
+ end
+
+ def formatter(formatter, options=nil)
+ @add_extension = FormatExtension.new(options).fetch(:extension) { false }
+ @formatter = formatter
+ accept(formatter.accept)
+ content_type(formatter.content_type)
+ end
+
+ def composed_headers
+ @headers ||= { 'accept'=> '*/*' , 'user-agent'=> 'Ruby' }
+ end
+
+ def headers(headers)
+ tap { composed_headers.merge!(headers) }
+ end
+
+ def accept(accept)
+ tap { composed_headers.store('accept', accept) }
+ end
+
+ def content_type(content_type)
+ tap { composed_headers.store('content-type', content_type) }
+ end
+
+ def basic_auth(options)
+ options = BasicAuth.new(options)
+
+ basic_encode = 'Basic ' + ["#{options.fetch(:username)}:#{options.fetch(:password)}"].pack('m').delete("\r\n")
+ tap { composed_headers.store('authorization', basic_encode) }
+ end
+ end
+ end
+end
126 lib/resto/request/option.rb
@@ -0,0 +1,126 @@
+# encoding: utf-8
+
+require 'openssl'
+
+module Resto
+ module Request
+ module Option
+
+ def options
+ @options ||= {}
+ end
+
+ begin :ssl_attributes
+
+ def use_ssl(use_ssl=true)#
+ tap do
+ { :verify_mode => OpenSSL::SSL::VERIFY_PEER }.update(options)
+ options.store(:use_ssl, use_ssl)
+ end
+ end
+
+ # Sets the SSL version. See OpenSSL::SSL::SSLContext#ssl_version=
+ def ssl_version(ssl_version)#1.9
+ tap { options.store(:ssl_version, ssl_version) }
+ end
+
+ # Sets an OpenSSL::PKey::RSA or OpenSSL::PKey::DSA object.
+ # (This method is appeared in Michal Rokos's OpenSSL extension.)
+ def key(key)#
+ tap { options.store(:key, key) }
+ end
+
+ # Sets an OpenSSL::X509::Certificate object as client certificate.
+ # (This method is appeared in Michal Rokos's OpenSSL extension).
+ def cert(cert)#
+ tap { options.store(:cert, cert) }
+ end
+
+ # Sets path of a CA certification file in PEM format.
+ # The file can contain several CA certificates.
+ def ca_file(ca_file)#
+ tap { options.store(:ca_file, ca_file) }
+ end
+
+ # Sets path of a CA certification directory containing certifications in
+ # PEM format.
+ def ca_path(ca_path)#
+ tap { options.store(:ca_path, ca_path) }
+ end
+
+ # Sets the X509::Store to verify peer certificate.
+ def cert_store(cert_store)#
+ tap { options.store(:cert_store, cert_store) }
+ end
+
+ # Sets the available ciphers. See OpenSSL::SSL::SSLContext#ciphers=
+ def ciphers(ciphers) # 1.9
+ tap { options.store(:ciphers, ciphers) }
+ end
+
+ # Sets the flags for server the certification verification at beginning of
+ # SSL/TLS session.
+ #
+ # OpenSSL::SSL::VERIFY_NONE or OpenSSL::SSL::VERIFY_PEER are acceptable.
+ def verify_mode(verify_mode)#
+ tap { options.store(:verify_mode, verify_mode) }
+ end
+
+ def verify_none
+ tap { verify_mode(OpenSSL::SSL::VERIFY_NONE) }
+ end
+
+ def verify_peer
+ tap { verify_mode(OpenSSL::SSL::VERIFY_PEER) }
+ end
+
+ # Sets the verify callback for the server certification verification.
+ def verify_callback(verify_callback)#
+ tap { options.store(:verify_callback, verify_callback)}
+ end
+
+ def verify_depth(verify_depth)#
+ tap { options.store(:verify_depth, verify_depth)}
+ end
+
+ # Sets the SSL timeout seconds.
+ def ssl_timeout(ssl_timeout)#
+ tap { options.store(:ssl_timeout, ssl_timeout)}
+ end
+ end #:ssl_attributes
+
+ def close_on_empty_response(close_on_empty_response)#
+ tap { options.store(:close_on_empty_response, close_on_empty_response) }
+ end
+
+ # Number of seconds to wait for the connection to open.
+ # If the HTTP object cannot open a connection in this many seconds,
+ # it raises a TimeoutError exception.
+ def open_timeout(time)#
+ tap { options.store(:open_timeout, time)}
+ end
+
+ # Number of seconds to wait for one block to be read (via one read(2) call).
+ # If the HTTP object cannot read data in this many seconds,
+ # it raises a TimeoutError exception.
+ def read_timeout(time)#
+ tap { options.store(:read_timeout, time)}
+ end
+
+ #Seconds to wait until reading one block (by one read(2) call).
+ def timeout(timeout) # 1.8.7 1.9??
+ tap { options.store(:timeout, timeout)}
+ end
+
+ # *WARNING* This method opens a serious security hole.
+ # Never use this method in production code.
+ #
+ # Sets an output stream for debugging.
+ #
+ # Resto.url('www.google.com').set_debug_output($stderr).get
+ def set_debug_output(output)
+ tap { options.store(:set_debug_output, output)}
+ end
+ end
+ end
+end
50 lib/resto/request/uri.rb
@@ -0,0 +1,50 @@
+# encoding: utf-8
+require 'uri'
+require 'cgi'
+
+module Resto
+ module Request
+ module Uri
+ def read_host
+ return nil unless @host
+
+ normalize_uri(@host)
+ @uri.host
+ end
+
+ def read_port; @port ||= 80; end
+
+ def composed_path
+ path = ["/#{@path}", @append_path].compact.join('/').gsub('//', "/")
+ path = "#{path}.#{current_formatter.extension}" if @add_extension
+ params = hash_to_params
+ return path unless (@query or params)
+
+ [path, [@query, params].compact.join('&')].join('?')
+ end
+
+ attr_reader :scheme
+ def normalize_uri(url)
+ @uri = URI.parse(url.match(/^https?:/) ? url : "http://#{url}")
+ @scheme ||= @uri.scheme
+ @host ||= @uri.host
+ @port ||= @uri.port
+ @path ||= @uri.path
+ @query ||= @uri.query
+ end
+
+ def parse_url(url)
+ normalize_uri(url)
+ end
+
+ def hash_to_params
+ return nil unless @params
+ raise ArgumentError unless @params.is_a?(Hash)
+
+ @params.sort.map do |a|
+ "#{CGI.escape(a.fetch(0).to_s)}=#{CGI.escape(a.fetch(1).to_s)}"
+ end.join('&')
+ end
+ end
+ end
+end
69 lib/resto/response/base.rb
@@ -0,0 +1,69 @@
+# encoding: utf-8
+
+require 'resto/format'
+require 'resto/translator/response_factory'
+
+module Resto
+ module Response
+ class Base
+
+ def klass(klass)
+ tap { @klass = klass }
+ end
+
+ def translator(translator)
+ @translator = Resto::Translator::ResponseFactory.create(translator)
+ self
+ end
+
+ def format(symbol)
+ formatter(Resto::Format.get(@symbol = symbol))
+ end
+
+ def formatter(formatter)
+ tap { @formatter = formatter }
+ end
+
+ def current_formatter
+ @formatter ||= Resto::Format.get(@symbol || :default)
+ end
+
+ def http_response(response)
+ tap { @response = response }
+ end
+
+ def read_body
+ body ? current_formatter.decode(body) : nil
+ end
+
+ def body
+ @response ? @response.body : nil
+ end
+
+ def code
+ @response ? @response.code : nil
+ end
+
+ attr_reader :response
+
+ def get
+ return self unless @translator
+
+ @translator.call(@klass, read_body).tap do |instance|
+ instance.response = self
+ end
+ end
+
+ def all
+ return self unless @translator
+
+ (read_body || []).map do |hash|
+ @translator.call(@klass, hash).tap do |instance|
+ instance.response = self
+ end
+ end
+ end
+
+ end
+ end
+end
44 lib/resto/translator/request_factory.rb
@@ -0,0 +1,44 @@
+# encoding: utf-8
+
+module Resto
+ module Translator
+ class Factory; end
+
+ class << Factory
+ def create(translator)
+ if translator.is_a?(Symbol) and :default == translator
+ new([])
+ elsif translator.is_a?(Array)
+ new(translator)
+ elsif translator.is_a?(Proc)
+ translator
+ elsif translator.is_a?(Class)
+ translator.new
+ else
+ raise(ArgumentError,
+ "Invalid argument. Valid symbol is :default. Array, Class or a
+ Proc is also a valid translator.")
+ end
+ end
+ end
+
+ class Factory
+ def initialize(keys)
+ @keys = keys
+ end
+ end
+
+ class RequestFactory < Factory
+
+ def call(attributes)
+ attributes ||= {}
+ raise ArgumentError unless attributes.is_a?(Hash)
+
+ @keys.reverse.inject(attributes) do |memo, item|
+ Hash.new.tap {|h| h[item] = memo }
+ end
+ end
+
+ end
+ end
+end
28 lib/resto/translator/response_factory.rb
@@ -0,0 +1,28 @@
+# encoding: utf-8
+
+require 'resto/translator/request_factory'
+module Resto
+ module Translator
+ class ResponseFactory < Factory
+
+ def call(klass, hash)
+ hash ||= {}
+ raise ArgumentError unless hash.is_a?(Hash)
+
+ klass.new(traverse(hash, 0))
+ end
+
+ private
+
+ def traverse(hash, index)
+ next_hash = hash[@keys.at(index)] || hash[@keys.at(index).to_s]
+
+ if next_hash
+ traverse(next_hash, index + 1)
+ else
+ hash
+ end
+ end
+ end
+ end
+end
37 lib/resto/validate.rb
@@ -0,0 +1,37 @@
+# encoding: utf-8
+
+module Resto
+ module Validate
+
+ autoload :Inclusion, 'resto/validate/inclusion'
+ autoload :Length, 'resto/validate/length'
+ autoload :Presence, 'resto/validate/presence'
+
+ def initialize
+ @if = lambda { |resource| true }
+ @unless = lambda { |resource| false }
+ end
+
+ def validate?(resource)
+ (@if.call(resource) && !@unless.call(resource))
+ end
+
+ def message(error_message)
+ tap { @message = error_message }
+ end
+
+ def if(&block)
+ tap { @if = block }
+ end
+
+ def unless(&block)
+ tap { @unless = block }
+ end
+
+ # .on - Specifies when this validation is active (default is :save, other
+ # options :create, :update).
+ # def on(args)
+ # tap { }
+ # end
+ end
+end
39 lib/resto/validate/inclusion.rb
@@ -0,0 +1,39 @@
+# encoding: utf-8
+
+module Resto
+ module Validate
+ class Inclusion
+ include Validate
+
+ def initialize
+ @allow_nil = false
+ @allow_blank = false
+ end
+
+ def in(range)
+ tap { @range = range }
+ end
+
+ def allow_nil
+ tap { @allow_nil = true }
+ end
+
+ def allow_blank
+ tap { @allow_blank = true }
+ end
+ end
+ end
+end
+#
+# .in - An enumerable object of available items. %w( m f ), 0..99, %w( jpg gif png )
+# .message - Specifies a custom error message (default is: “is not included in the list”).
+# .allow_nil - If set to true, skips this validation if the attribute is nil (default is false).
+# .allow_blank - If set to true, skips this validation if the attribute is blank (default is false).
+# .if - Specifies a method, proc or string to call to determine if the validation should occur
+# (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The method,
+# proc or string should return or evaluate to a true or false value.
+# .unless - Specifies a method, proc or string to call to determine if the validation should not occur
+# (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }).
+# The method, proc or string should return or evaluate to a true or false value.
+
+
36 lib/resto/validate/length.rb
@@ -0,0 +1,36 @@
+# encoding: utf-8
+
+module Resto
+ module Validate
+ class Length
+ include Validate
+
+ def is(number)
+ tap { @number = number }
+ end
+
+ end
+ end
+end
+# .is - The exact size of the attribute.
+# .within - A range specifying the minimum and maximum size of the attribute.
+# .allow_nil - Attribute may be nil; skip validation.
+# .allow_blank - Attribute may be blank; skip validation.
+# .if - Specifies a method, proc or string to call to determine if the
+# validation should occur (e.g. :if => :allow_validation, or :if =>
+# Proc.new { |user| user.signup_step > 2 }). The method, proc or string should
+# return or evaluate to a true or false value.
+# .unless - oposite of .if
+#
+# messages
+# :too_long - The error message if the attribute goes over the maximum
+# (default is: “is too long (maximum is %count characters)”).
+# :too_short - The error message if the attribute goes under the minimum
+# (default is: “is too short (min is %count characters)”).
+# :wrong_length - The error message if using the :is method and the attribute
+# is the wrong size (default is: “is the wrong length
+# (should be %count characters)”).
+# :message - The error message to use for a :minimum, :maximum, or
+# :is violation. An alias of the appropriate too_long/too_short/wrong_length
+# message.
+
24 lib/resto/validate/presence.rb
@@ -0,0 +1,24 @@
+# encoding: utf-8
+
+module Resto
+ module Validate
+ class Presence
+ include Validate
+
+ def attribute_value(resource, attribute_method)
+
+ error_key = (attribute_method.to_s + "_presence").to_sym
+ value_before_cast = resource.send("#{attribute_method}_without_cast")
+
+ error =
+ if (validate?(resource) && value_before_cast.to_s.strip.empty?)
+ ":#{attribute_method} #{@message || "can’t be blank"}"
+ else
+ nil
+ end
+
+ resource.add_error(error_key, error)
+ end
+ end
+ end
+end
5 lib/resto/version.rb
@@ -0,0 +1,5 @@
+# encoding: utf-8
+
+module Resto
+ VERSION = "0.0.1"
+end
8 readme.markdown
@@ -0,0 +1,8 @@
+
+/bin
+require "optparse" - parsing from commandline
+
+
+# Build and install
+$ gem build proto.gemspec
+$ gem install proto-0.0.1.gem
40 resto.gemspec
@@ -0,0 +1,40 @@
+# encoding: utf-8
+
+
+require File.expand_path("../lib/resto/version", __FILE__)
+
+Gem::Specification.new do |s|
+ s.name = "resto"
+ s.version = Resto::VERSION
+ s.platform = Gem::Platform::RUBY
+ s.authors = ["Anders Törnqvist"]
+ s.email = ["anders@elabs.se"]
+ s.homepage = "http://rubygems.org/gems/resto"
+ s.summary = "Restful Web Service"
+ s.description = "Restful Web Service"
+
+ s.required_ruby_version = ::Gem::Requirement.new(">= 1.8.7")
+ s.required_rubygems_version = ">= 1.3.6"
+ s.rubyforge_project = "resto"
+
+ s.add_runtime_dependency "yajl-ruby", "0.7.8"
+ # s.add_dependency "activesupport", "3.0.0" ???
+ s.add_development_dependency "bundler", ">= 1.0.0"
+ s.add_development_dependency "rspec", ">= 2.0.1"
+ s.add_development_dependency "webmock", "= 1.3.5" # higher versions breaks textmate tspec tests...
+ s.add_development_dependency "code-cleaner", "0.8.2"
+ s.add_development_dependency "reek"
+ s.add_development_dependency "metrical"
+ s.add_development_dependency "simplecov"
+
+ # CLI testing
+ # http://github.com/radar/guides/blob/master/gem-development.md
+ # s.add_dependency "thor"
+ # s.add_development_dependency "cucumber"
+ # s.add_development_dependency "aruba"
+
+ # s.files = Dir.glob("{lib,spec}/**/*") + %w(resto.gemspec)
+ # s.files = `git ls-files`.split("\n")
+ #s.executables = `git ls-files`.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact
+ s.require_path = 'lib'
+end
58 spec/resto/extra/copy_spec.rb
@@ -0,0 +1,58 @@
+# encoding: utf-8
+
+require 'spec_helper'
+require 'resto/extra/copy'
+describe Resto::Extra::Copy do
+ describe ".request_base" do
+ before do
+ @request_base = Resto::Request::Base.new.port(40)
+ .url('http://www.aftonbladet.se:92/customers')
+ .query('q=adam')
+ .path('contacts')
+
+ @new_request_base = Resto::Extra::Copy.request_base(@request_base)
+ .url('http://new.se:99/other')
+ .query('q=not-same')
+ .path('other-contacts/')
+ .headers({ "accept"=> "other", "user-agent"=> "Ruby" })
+ .append_path(2)
+ end
+
+ it { @new_request_base.object_id.should_not == @request_base.object_id }
+ it { @request_base.read_port.should == 40 }
+ it { @request_base.composed_path.should == '/contacts?q=adam' }
+
+ it do
+ @request_base.composed_headers.should == { "accept"=> "*/*",
+ "user-agent"=> "Ruby" }
+ end
+
+ it do
+ @request_base.composed_headers.object_id.should_not ==
+ @new_request_base.composed_headers.object_id
+ end
+
+ it { @new_request_base.read_port.should == 40 }
+
+ it do
+ @new_request_base.composed_headers.should == { "accept"=> "other",
+ "user-agent"=> "Ruby" }
+ end
+
+ it do
+ @new_request_base.composed_path.should == "/other-contacts/2?q=not-same"
+ end
+ end
+
+ describe ".response_base" do
+ before do
+ @response_base = Resto::Response::Base.new.format(:json)
+ @new_response_base = Resto::Extra::Copy.response_base(@response_base)
+ .http_response('response')
+ end
+
+ it { @response_base.instance_eval { @response }.should == nil }
+ it { @new_response_base.object_id.should_not == @response_base.object_id }
+ it { @new_response_base.instance_eval { @response }.should == 'response' }
+ end
+end
71 spec/resto/extra/hash_args_spec.rb
@@ -0,0 +1,71 @@
+# encoding: utf-8
+
+require 'spec_helper'
+require 'resto/extra/hash_args'
+
+describe Resto::Extra::HashArgs do
+
+ class_context(%Q{
+ class BasicAuthication < Resto::Extra::HashArgs
+ key :username
+ key :password
+ end}) do
+
+ it "returns the value from the block when no value is found by key" do
+ BasicAuthication.new(nil).fetch(:username) { 'anders' }.should == 'anders'
+ end
+
+ it "returns the value found by the key" do
+ BasicAuthication.new({'username' => 'anders', :password => 'secret'})
+ .fetch(:password) { 'other' }.should == 'secret'
+ end
+
+ it "the key is translated to its symbol" do
+ BasicAuthication.new({'username' => 'anders', :password => 'secret'})
+ .fetch(:username) { 'other' }.should == 'anders'
+ end
+ end
+
+ class_context(%Q{
+ class FormatExt < Resto::Extra::HashArgs
+ key :extension
+ end}) do
+
+ it "returns the value from the block" do
+ FormatExt.new({}).fetch(:extension) { 'block' }.should == 'block'
+ end
+
+ if RUBY_VERSION < '1.9'
+
+ it "raises IndexError when no value and no block" do
+ expect { FormatExt.new({}).fetch(:extension) }
+ .to raise_error(IndexError, 'key not found')
+ end
+
+ else
+
+ it "raises KeyError when no value and no block" do
+ lambda { FormatExt.new({}).fetch(:extension) }
+ .should raise_error(KeyError, 'key not found: :extension')
+ end
+
+ end
+
+ it "raises" do
+ expect { FormatExt.new({:username => "anders"}) }
+ .to raise_error(ArgumentError, /The key 'username'/)
+
+ expect { FormatExt.new("string") }
+ .to raise_error(ArgumentError, "'string' must be a Hash")
+
+ expect { FormatExt.new(:extension => 'value', 'extension' => 'value') }
+ .to raise_error(ArgumentError, "duplicated keys: extension, extension")
+
+ expect { FormatExt.new({:invalid_key => 'invalid' }) }
+ .to raise_error(ArgumentError, /The key 'invalid_key' is not valid/)
+
+ expect { FormatExt.new({:extension => 'value' }).fetch(:invalid_key) }
+ .to raise_error(ArgumentError, /The key 'invalid_key' is not valid/)
+ end
+ end
+end
24 spec/resto/format/default_spec.rb
@@ -0,0 +1,24 @@
+# encoding: utf-8
+
+require 'spec_helper'
+require 'resto/format'
+
+describe "Resto::Format.get" do
+ subject { Resto::Format.get }
+
+ its(:extension) { should == nil }
+ its(:accept) { should == '*/*' }
+ its(:content_type) { should == nil }
+ describe ".encode(text)" do
+ it("returns text") do
+ subject.encode('very important').should == 'very important'
+ end
+ end
+
+ describe ".decode(text)" do
+ it("returns text") do
+ subject.encode('somehting important').should == 'somehting important'
+ end
+ end
+
+end
29 spec/resto/format/json_spec.rb
@@ -0,0 +1,29 @@
+# encoding: utf-8
+
+require 'spec_helper'
+require 'resto/format'
+require 'yajl'
+
+describe "Resto::Format.get(:json)" do
+ subject { Resto::Format.get(:json) }
+
+ its(:extension) { should == 'json' }
+ its(:accept) { should == 'application/json, */*' }
+ its(:content_type) { should == 'application/json' }
+
+ describe ".encode(hash)" do
+ before { @result = subject.encode({ :bar => "some string",
+ :foo => 12425125}) }
+
+ it { @result.should =~ /bar\":\"some string/ }
+ it { @result.should =~ /foo\":12425125/ }
+ end
+
+ describe '.decode(json)' do
+ json = Yajl::Encoder.encode( { :foo => 12425125, :bar => "some string" })
+ expected = { 'foo' => 12425125, 'bar' => "some string" }
+
+ it { subject.decode(json).should == expected }
+ end
+
+end
21 spec/resto/format/plain_spec.rb
@@ -0,0 +1,21 @@
+# encoding: utf-8
+
+require 'spec_helper'
+require 'resto/format'
+
+describe "Resto::Format.get(:plain)" do
+ subject { Resto::Format.get(:plain) }
+
+ its(:extension) { should == nil }
+ its(:accept) { should == 'text/plain, */*' }
+ its(:content_type) { should == 'text/plain' }
+
+ context ".encode(text)" do
+ it("returns text") { subject.encode('important').should == 'important' }
+ end
+
+ context ".decode(text)" do
+ it("returns text") { subject.encode('important').should == 'important' }
+ end
+
+end
13 spec/resto/format/xml_spec.rb
@@ -0,0 +1,13 @@
+# encoding: utf-8
+
+require 'spec_helper'
+require 'resto/format'
+
+describe "Resto::Format.get(:xml)" do
+ subject { Resto::Format.get(:xml) }
+
+ its(:extension) { should == 'xml' }
+ its(:accept) { should == 'application/xml, */*' }
+ its(:content_type) { should == 'application/xml;charset=utf-8' }
+
+end
57 spec/resto/property/handler_spec.rb
@@ -0,0 +1,57 @@
+# encoding: utf-8
+require 'spec_helper'
+
+class Article
+ def initialize; @errors = {}; end
+ def title_without_cast; " 200 "; end
+ def empty_title_without_cast; " "; end
+ def errors; @errors; end
+ def add_error(key, error); @errors.store(key, error); end
+end
+
+class AProperty;
+ include Resto::Property;
+ def cast(value, errors); value.to_i; end
+end
+
+describe Resto::Property::Handler do
+ let(:article) { Article.new }
+ let(:property) { AProperty.new(:title) }
+ let(:property2) { AProperty.new(:empty_title) }
+
+ context "#add(property)" do
+ before { subject.add(property) }
+
+ describe "#attribute_key(property_key)" do
+ it { subject.attribute_key('title').should == :title }
+ end
+
+ describe "cast(:property_key, '200', errors)" do
+ it { subject.cast(:title, '200', nil).should == 200 }
+ end
+
+ describe "#attribute_key('non_existing_title')" do
+ it { subject.attribute_key('non_existing_title').should == false }
+ end
+
+ context "#add(other_property)" do
+ describe "#validate(article)" do
+ before do
+ property.validate_presence
+ property2.validate_presence
+ subject.add(property2)
+ subject.validate(article)
+ end
+
+ context "then article has the following error" do
+ it { article.errors.fetch(:title_presence, false).should == nil }
+
+ it do
+ article.errors.fetch(:empty_title_presence, false)
+ .should == ":empty_title can’t be blank"
+ end
+ end
+ end
+ end
+ end
+end
67 spec/resto/property/integer_spec.rb
@@ -0,0 +1,67 @@
+# encoding: utf-8
+
+require 'spec_helper'
+require 'resto/property'
+
+describe Resto::Property::Integer do
+
+ context "Resto::Property::Integer.new(:age)" do
+ let(:errors) { {} }
+ subject { Resto::Property::Integer.new(:age) }
+
+ context ".cast(20, errors)" do
+ it("returns 20 with no errors") do
+ subject.cast(20, errors).should == 20
+ errors.fetch(:age_integer, false).should == nil
+ end
+ end
+
+ context ".cast('22', errors)" do
+ it("returns 22 with no errors") do
+ subject.cast('22', errors).should == 22
+ errors.fetch(:age_integer, false).should == nil
+ end
+ end
+
+ context ".cast('', errors)" do
+ it("returns nil with no errors") do
+ subject.cast('', errors).should == nil