carbon-copy 0.0.2

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/.gitignore ADDED
@@ -0,0 +1 @@
1
+ .request_cache/
data/README.md ADDED
@@ -0,0 +1,24 @@
1
+ # Carbon Copy - Easily cache them REST calls
2
+
3
+ We created Carbon Copy to allow for quick front end (or back end) development by
4
+ caching REST responses. We also wanted it to be easy and quick.
5
+
6
+ ### How it works
7
+
8
+ Install the gem
9
+
10
+ gem install carbon-copy
11
+
12
+ Run the server with a specified port
13
+
14
+ carbon-copy -p 8989
15
+
16
+ Alter your REST calls on the front-end to pipe them locally through carbon-copy
17
+
18
+ $.get('http://localhost:8989/slow.server.com/resource')
19
+
20
+ On the first run, the requests will be cached in the `.request_cache` directory
21
+ in the same path as where you called the server. On subsequent requests, the
22
+ calls will pull from that cache instead of contacting the server.
23
+
24
+ You can restart the cache at any time by deleting the specific cache files.
data/bin/carbon-copy ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env ruby
2
+ require 'optparse'
3
+ require 'carbon-copy'
4
+
5
+ server = CarbonCopy::CacheServer.new
6
+
7
+ opts = OptionParser.new do |opts|
8
+ opts.banner = "Carbon Copy: Cache them RESTs"
9
+ opts.on("-p", "--port Port", "Port to run server") do |v|
10
+ server.run(v)
11
+ end
12
+ end
13
+ opts.parse!
14
+
15
+ uri = ARGV.shift
16
+
17
+ if uri.to_s.strip.empty?
18
+ puts opts
19
+ exit 1
20
+ end
@@ -0,0 +1,17 @@
1
+ $:.push File.expand_path("../lib", __FILE__)
2
+ require 'carbon-copy'
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Kevin Webster"]
6
+ gem.email = ["me@kdoubleyou.com"]
7
+
8
+ gem.description = "easily cache them REST calls"
9
+ gem.summary = "REST cache"
10
+ gem.homepage = 'https://github.com/rabidpraxis/carbon-copy'
11
+ gem.files = `git ls-files`.split("\n")
12
+ gem.test_files = `git ls-files -- spec/*`.split("\n")
13
+ gem.name = "carbon-copy"
14
+ gem.require_paths = ["lib"]
15
+ gem.version = CarbonCopy::VERSION
16
+ gem.executables = ['carbon-copy']
17
+ end
@@ -0,0 +1,6 @@
1
+ require 'carbon-copy/cache_server'
2
+ require 'carbon-copy/http_cacher'
3
+
4
+ module CarbonCopy
5
+ VERSION = '0.0.2'
6
+ end
@@ -0,0 +1,71 @@
1
+ require 'socket'
2
+ require 'open-uri'
3
+ require 'openssl'
4
+ require 'carbon-copy/http_cacher'
5
+
6
+ module CarbonCopy
7
+ class CacheServer
8
+
9
+ def run(port)
10
+ webserver = TCPServer.new('127.0.0.1', port)
11
+ puts "Running Carbon Copy on localhost port #{port}"
12
+ while (session = webserver.accept)
13
+ Thread.new(session, &method(:handle))
14
+ end
15
+ end
16
+
17
+ def parse_request(session)
18
+ request = session.readline
19
+
20
+ parsed = {}
21
+ #--- Initial host/uri information -------------------------------------
22
+ parsed[:verb] = request[/^\w+/]
23
+ parsed[:url] = request[/^#{parsed[:verb]}\s+\/(\S+)/, 1]
24
+ parsed[:host] = request[/^#{parsed[:verb]}\s+\/([^\/ ]+)/, 1]
25
+ parsed[:version] = request[/HTTP\/(1\.\d)\s*$/, 1]
26
+ parsed[:uri] = request[/^#{parsed[:verb]}\s+\/#{parsed[:host]}(\S+)\s+HTTP\/#{parsed[:version]}/, 1] || '/'
27
+
28
+ uri = URI::parse(parsed[:uri])
29
+ parsed[:request_str] = "#{parsed[:verb]} #{uri.path}?#{uri.query} HTTP/#{parsed[:version]}\r"
30
+
31
+ #--- Header and final response text -----------------------------------
32
+ parsed[:headers] = parse_headers(session)
33
+
34
+ #--- Update header info -----------------------------------------------
35
+ parsed[:headers]["Host"] = parsed[:host]
36
+
37
+ parsed[:header_str] = parsed[:headers].map{|a, b| "#{a}: #{b}"}.join("\r\n")
38
+ parsed[:response] = "#{parsed[:request_str]}\n#{parsed[:header_str]}\r\n\r\n"
39
+
40
+ parsed
41
+ end
42
+
43
+ def parse_headers(request)
44
+ header = {}
45
+ unless request.eof?
46
+ loop do
47
+ line = request.readline
48
+ if line.strip.empty?
49
+ break
50
+ end
51
+
52
+ /^(\S+): ([^\r\n]+)/.match(line)
53
+ header[$1] = $2
54
+ end
55
+ end
56
+ header
57
+ end
58
+
59
+ def handle(session)
60
+ begin
61
+ req = parse_request(session)
62
+ resp = HTTPCacher.new.connect(req)
63
+ session.write(resp)
64
+ session.close
65
+ rescue => e
66
+ p e.message
67
+ p e.backtrace
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,91 @@
1
+ require 'digest/md5'
2
+
3
+ module CarbonCopy
4
+ class HTTPCacher
5
+ attr_reader :base_dir
6
+
7
+ def initialize(base_dir = Dir.pwd)
8
+ @base_dir = base_dir
9
+ end
10
+
11
+ #--------------------------------------------------------------------------
12
+ # Setup cache directory
13
+ #--------------------------------------------------------------------------
14
+ def cache_dir
15
+ "#{base_dir}/.request_cache"
16
+ end
17
+
18
+ #--------------------------------------------------------------------------
19
+ # Determine final path
20
+ #--------------------------------------------------------------------------
21
+ def path(parsed)
22
+ uri = ( parsed[:uri] == '/' ) ? '' : parsed[:uri].gsub("\/", "_")
23
+ hash = Digest::MD5.new << parsed[:header_str]
24
+ #--- Cache directory structure ----------------------------------------
25
+ """
26
+ #{cache_dir}/
27
+ #{parsed[:host]}/
28
+ #{parsed[:verb].downcase}
29
+ #{uri}_
30
+ #{hash}
31
+ """.gsub(/\n|\s/, '')
32
+ end
33
+
34
+ #--------------------------------------------------------------------------
35
+ # Ensure cached directories are created
36
+ #--------------------------------------------------------------------------
37
+ def verify_cached_dir(parsed)
38
+ Dir.mkdir(cache_dir) unless File.exists?(cache_dir)
39
+ host_cache = "#{cache_dir}/#{parsed[:host]}"
40
+ Dir.mkdir(host_cache) unless File.exists?(host_cache)
41
+ end
42
+
43
+ def get_response(parsed)
44
+ a = TCPSocket.new(parsed[:host], 80)
45
+ a.write(parsed[:response])
46
+
47
+ #--- Pull request data ------------------------------------------------
48
+ content_len = nil
49
+ buff = ""
50
+ loop do
51
+ line = a.readline
52
+ buff += line
53
+ if line =~ /^Content-Length:\s+(\d+)\s*$/
54
+ content_len = $1.to_i
55
+ end
56
+ break if line.strip.empty?
57
+ end
58
+
59
+ #--- Pull response ----------------------------------------------------
60
+ if content_len
61
+ buff += a.read(content_len)
62
+ else
63
+ loop do
64
+ if a.eof? || line = a.readline || line.strip.empty?
65
+ break
66
+ end
67
+ buff += line
68
+ end
69
+ end
70
+ a.close
71
+
72
+ buff
73
+ end
74
+
75
+ def connect(parsed)
76
+ verify_cached_dir(parsed)
77
+ cached_path = path(parsed)
78
+
79
+ if File.exists?(cached_path) && !File.zero?(cached_path)
80
+ puts "Getting file #{cached_path} from cache"
81
+ IO.read( cached_path )
82
+ else
83
+ resp = get_response(parsed)
84
+ File.open( cached_path, 'w' ) do |f|
85
+ f.puts resp
86
+ end
87
+ resp
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,101 @@
1
+ require File.expand_path('../../lib/carbon-copy', __FILE__)
2
+ require 'open-uri'
3
+ require 'net/http'
4
+
5
+ describe CarbonCopy::CacheServer do
6
+ describe '#parse_headers' do
7
+ let(:rs) { CarbonCopy::CacheServer.new }
8
+
9
+ it 'should parse easy header strings' do
10
+ st = StringIO.new
11
+ st << "TestHeader: Header Result"
12
+ st << "\r\n\r\n"
13
+ st.rewind
14
+ rs.parse_headers(st).should eq({"TestHeader" => "Header Result"})
15
+ end
16
+
17
+ it 'should parse multi-line headers' do
18
+ st = StringIO.new
19
+ st << "TestHeader: Header Result\n"
20
+ st << "TestHeaders: Header Result"
21
+ st << "\r\n\r\n"
22
+ st.rewind
23
+ rs.parse_headers(st).should eq({
24
+ "TestHeader" => "Header Result",
25
+ "TestHeaders" => "Header Result"
26
+ })
27
+ end
28
+ end
29
+
30
+ describe '#parse_request' do
31
+ let(:rs) { CarbonCopy::CacheServer.new }
32
+ let(:req) { rs.parse_request(request) }
33
+
34
+ describe 'host with path' do
35
+ let(:request) {
36
+ req = StringIO.new << "GET /apple.com/google/face/ HTTP/1.1\n"
37
+ req.rewind
38
+ req
39
+ }
40
+
41
+ it 'verb' do
42
+ req[:verb].should eq("GET")
43
+ end
44
+
45
+ it 'url with path' do
46
+ req[:url].should eq('apple.com/google/face/')
47
+ end
48
+
49
+ it 'version' do
50
+ req[:version].should eq('1.1')
51
+ end
52
+
53
+ it 'host' do
54
+ req[:host].should eq('apple.com')
55
+ end
56
+
57
+ it 'uri' do
58
+ req[:uri].should eq('/google/face/')
59
+ end
60
+
61
+ it 'request_str' do
62
+ req[:request_str].should eq("GET /google/face/? HTTP/1.1\r")
63
+ end
64
+ end
65
+
66
+ describe 'just host' do
67
+ let(:request) {
68
+ req = StringIO.new << "GET /apple.com HTTP/1.1\n"
69
+ req.rewind
70
+ req
71
+ }
72
+ it 'host' do
73
+ req[:host].should eq('apple.com')
74
+ end
75
+
76
+ it 'uri' do
77
+ req[:uri].should eq('/')
78
+ end
79
+ end
80
+
81
+ end
82
+
83
+ describe '#handle' do
84
+ before(:all) do
85
+ Thread.new do
86
+ CarbonCopy::CacheServer.new.run(7979)
87
+ end
88
+ end
89
+
90
+ it 'caches google.com simple get request' do
91
+ url = 'www.apple.com'
92
+ o_req = get("http://#{url}").body
93
+ req = get("http://localhost:7979/#{url}").body
94
+ req.should eq(o_req)
95
+ end
96
+ end
97
+ end
98
+
99
+ def get(url)
100
+ Net::HTTP.get_response(URI.parse(url))
101
+ end
@@ -0,0 +1,27 @@
1
+ require File.expand_path('../../lib/carbon-copy', __FILE__)
2
+ require 'digest/md5'
3
+
4
+ describe CarbonCopy::HTTPCacher do
5
+ let(:cacher) { CarbonCopy::HTTPCacher.new }
6
+ let(:parsed) {
7
+ {
8
+ verb: 'GET',
9
+ url: 'gist.github.com/74107',
10
+ host: 'gist.github.com',
11
+ version: '1.1',
12
+ uri: '/74107',
13
+ header_str: 'Test Header: Test Result'
14
+ }
15
+ }
16
+ it 'should have path with url' do
17
+ hash = Digest::MD5.new << parsed[:header_str]
18
+ cacher.path(parsed).should match(/\.request_cache\/gist\.github\.com\/get_74107_#{hash}/)
19
+ end
20
+
21
+ it 'should reflect no path' do
22
+ hash = Digest::MD5.new << parsed[:header_str]
23
+ parsed[:url] = 'gist.github.com'
24
+ parsed[:uri] = '/'
25
+ cacher.path(parsed).should match(/\.request_cache\/gist\.github\.com\/get_#{hash}/)
26
+ end
27
+ end
metadata ADDED
@@ -0,0 +1,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: carbon-copy
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Kevin Webster
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-09-02 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: easily cache them REST calls
15
+ email:
16
+ - me@kdoubleyou.com
17
+ executables:
18
+ - carbon-copy
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - .gitignore
23
+ - README.md
24
+ - bin/carbon-copy
25
+ - carbon-copy.gemspec
26
+ - lib/carbon-copy.rb
27
+ - lib/carbon-copy/cache_server.rb
28
+ - lib/carbon-copy/http_cacher.rb
29
+ - spec/cache_server_spec.rb
30
+ - spec/http_cacher_spec.rb
31
+ homepage: https://github.com/rabidpraxis/carbon-copy
32
+ licenses: []
33
+ post_install_message:
34
+ rdoc_options: []
35
+ require_paths:
36
+ - lib
37
+ required_ruby_version: !ruby/object:Gem::Requirement
38
+ none: false
39
+ requirements:
40
+ - - ! '>='
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ required_rubygems_version: !ruby/object:Gem::Requirement
44
+ none: false
45
+ requirements:
46
+ - - ! '>='
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ requirements: []
50
+ rubyforge_project:
51
+ rubygems_version: 1.8.24
52
+ signing_key:
53
+ specification_version: 3
54
+ summary: REST cache
55
+ test_files:
56
+ - spec/cache_server_spec.rb
57
+ - spec/http_cacher_spec.rb
58
+ has_rdoc: