musashi 0.0.1.b1

Sign up to get free protection for your applications and to get access to all the features.
File without changes
data/LICENSE ADDED
File without changes
@@ -0,0 +1,87 @@
1
+ =Musashi
2
+
3
+ Musashi is a RestClient that follows relations or links.
4
+
5
+ =Example
6
+
7
+ ==Protocol
8
+
9
+ curl -i http://foo.com/customers/1
10
+ {
11
+ "id": "1",
12
+ "login": "foo",
13
+ "name": "Foo Bar",
14
+ "phones": [
15
+ {
16
+ "number": "51156782323",
17
+ "extension": ""
18
+ },
19
+ {
20
+ "number": "51156782323",
21
+ "extension": "2345"
22
+ }
23
+ ],
24
+ "emails": ["foo@gmail.com",
25
+ "foo.bar@gmail.com.br",
26
+ "foo+you@gmail.com.br"],
27
+ "address": {
28
+ "street": "No Where",
29
+ "number": "123",
30
+ "complement": "4-F",
31
+ "district": "Pinheiros",
32
+ "city": "São Paulo",
33
+ "state": "SP",
34
+ "country": "Brazil",
35
+ "postal_code": "000000"
36
+ },
37
+ "link": [
38
+ {
39
+ "rel": "items",
40
+ "href": "http://bar.com/customers/1/items"
41
+ }
42
+ ]
43
+ }
44
+
45
+ curl -i http://bar.com/customer/1/items
46
+ {"entries" :
47
+ [ "http://bazz.com/items/1", "http://bazz.com/items/2" ]
48
+ }
49
+
50
+ curl -i http://bazz.com/items/1
51
+ {"name":"bazz item"}
52
+
53
+ ==Code
54
+
55
+ ===With class
56
+
57
+ require 'musashi'
58
+ class Customer
59
+ include Musashi::Retriever
60
+ include Musashi::Resource::JSON
61
+ attr_accessor :id
62
+ endpoint = '"http://foo.com/customers/#{id}"'
63
+ end
64
+
65
+ customer.id = '1'
66
+ puts customer.login
67
+ puts customer.phones.first.number
68
+ puts customer.email.first
69
+ puts customer.address.number
70
+ customer.items.entries.each do |i|
71
+ puts i.name
72
+ end
73
+
74
+ ===Without class
75
+
76
+ require 'musashi'
77
+ customer = Musashi::Resource.new('http://foo.com/customer/1', :format => :json)
78
+
79
+ puts custome.id
80
+ puts customer.login
81
+ puts customer.phones.first.number
82
+ puts customer.email.first
83
+ puts customer.address.number
84
+ customer.items.entries.each do |i|
85
+ puts i.name
86
+ end
87
+
File without changes
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib/', __FILE__)
3
+ $:.unshift lib unless $:.include?(lib)
4
+
5
+ require 'rake'
6
+ require 'rspec/core/rake_task'
7
+ require 'musashi/version'
8
+
9
+ desc 'Run tests'
10
+ RSpec::Core::RakeTask.new(:spec)
11
+
12
+ desc 'Run the gem build'
13
+ task :build do
14
+ system 'gem build musashi.gemspec'
15
+ end
16
+
17
+ desc 'Run the gem push'
18
+ task :release => [:spec, :build] do
19
+ system "gem push musashi-#{Musashi::VERSION}.gem"
20
+ end
21
+
22
+ desc 'Run the unit suite'
23
+ task :default => [:spec]
24
+
@@ -0,0 +1,41 @@
1
+ module Faraday
2
+ class Response::RaiseHttp4xx < Response::Middleware
3
+ def on_complete(env)
4
+ case env[:status].to_i
5
+ when 400
6
+ raise "BadRequest-#{error_message(env)}"
7
+ when 401
8
+ raise "Unauthorized-#{error_message(env)}"
9
+ when 403
10
+ raise "Forbidden-#{error_message(env)}"
11
+ when 404
12
+ raise "NotFound-#{error_message(env)}"
13
+ when 406
14
+ raise "NotAcceptable-#{error_message(env)}"
15
+ when 420
16
+ raise "EnhanceYourCalm-#{error_message(env)}"
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def error_message(env)
23
+ "#{env[:method].to_s.upcase} #{env[:url].to_s}: #{env[:status]}#{error_body(env[:body])}"
24
+ end
25
+
26
+ def error_body(body)
27
+ if body.nil?
28
+ nil
29
+ elsif body['error']
30
+ ": #{body['error']}"
31
+ elsif body['errors']
32
+ first = Array(body['errors']).first
33
+ if first.kind_of? Hash
34
+ ": #{first['message'].chomp}"
35
+ else
36
+ ": #{first.chomp}"
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,23 @@
1
+ module Faraday
2
+
3
+ class Response::RaiseHttp5xx < Response::Middleware
4
+
5
+ def on_complete(env)
6
+ case env[:status].to_i
7
+ when 500
8
+ raise "InternalServerError-#{error_message(env)}"
9
+ when 502
10
+ raise "BadGateway-#{error_message(env)}"
11
+ when 503
12
+ raise "ServiceUnavailable-#{error_message(env)}"
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def error_message(env, body=nil)
19
+ "#{env[:method].to_s.upcase} #{env[:url].to_s}: #{[env[:status].to_s + ':', body].compact.join(' ')}"
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,16 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require 'active_support/core_ext/integer'
3
+ require 'active_support/core_ext/string'
4
+ require 'active_support/core_ext/hash'
5
+ require 'active_support/core_ext/module'
6
+ require 'faraday'
7
+ require 'faraday_middleware'
8
+ require 'yaml'
9
+
10
+ require 'faraday/response/raise_http_4xx'
11
+ require 'faraday/response/raise_http_5xx'
12
+ require 'musashi/version'
13
+ require 'musashi/connection'
14
+ require 'musashi/retriever'
15
+ require 'musashi/resource'
16
+
@@ -0,0 +1,38 @@
1
+ # -*- encoding: utf-8 -*-
2
+ module Musashi::Connection
3
+
4
+ attr_accessor :endpoint
5
+ attr_accessor_with_default :format, :json
6
+
7
+ def options
8
+ return @options if @options
9
+ builder = ::Faraday::Builder.new
10
+ builder.use ::Faraday::Request::Multipart
11
+ builder.use ::Faraday::Request::UrlEncoded
12
+ builder.use ::Faraday::Response::RaiseHttp4xx
13
+ case format.to_s.downcase
14
+ when 'json'
15
+ builder.use ::Faraday::Response::Mashify
16
+ builder.use ::Faraday::Response::ParseJson
17
+ when 'xml'
18
+ builder.use ::Faraday::Response::Mashify
19
+ builder.use ::Faraday::Response::ParseXml
20
+ end
21
+ builder.use ::Faraday::Response::RaiseHttp5xx
22
+ builder.adapter :net_http
23
+ @options = {
24
+ :headers => {
25
+ 'Accept' => "application/#{format}",
26
+ 'User-Agent' => "Musashi Gem #{::Musashi::VERSION}"
27
+ },
28
+ :url => endpoint,
29
+ :ssl => {:verify => false},
30
+ :builder => builder
31
+ }
32
+ end
33
+
34
+ def connection
35
+ @connection ||= ::Faraday.new(options)
36
+ end
37
+
38
+ end
@@ -0,0 +1,61 @@
1
+ class Musashi::Resource
2
+
3
+ module Format
4
+
5
+ def self.formats
6
+ @@formats ||= {}
7
+ end
8
+
9
+ def retrieve
10
+ marshalled = super
11
+ resource_type = Format.formats[format]
12
+ marshalled.extend(resource_type) unless resource_type.nil?
13
+ marshalled
14
+ end
15
+
16
+ end
17
+
18
+ module JSON
19
+ include Format
20
+ Format.formats[:json] = JSON
21
+
22
+ def self.included(clazz)
23
+ super
24
+ clazz.extend ClassMethods
25
+ clazz.send :include, InstanceMethods
26
+ end
27
+
28
+ module ClassMethods
29
+ attr_accessor :endpoint
30
+ end
31
+
32
+ module InstanceMethods
33
+ def retrieve
34
+ self.endpoint = eval self.class.endpoint
35
+ super
36
+ end
37
+ end
38
+
39
+ def method_missing(sym,*args,&block)
40
+ super
41
+ rescue NoMethodError => error
42
+ raise error unless retrieved?
43
+ raise NoMethodError.new(sym) unless respond_to?(:link)
44
+ selected = link.select{|ln| ln.rel == sym.to_s}
45
+ raise NoMethodError.new(sym) if selected.nil? or selected.empty?
46
+ raise "More than one result for #{sym} #{selected.join(',')}" if selected.size > 1
47
+ selected.first.href
48
+ end
49
+
50
+ end
51
+
52
+ include ::Musashi::Retriever::DelegateHash
53
+ def initialize(url,options={})
54
+ raise ArgumentError.new('Undefined url') if url.nil? or url.empty?
55
+ resource_type = Format.formats[ options[:format] || :json ]
56
+ extend(resource_type) unless resource_type.nil?
57
+ self.endpoint = url
58
+ end
59
+
60
+ end
61
+
@@ -0,0 +1,192 @@
1
+ # -*- encoding: utf-8 -*-
2
+ module Musashi::Retriever
3
+
4
+ module Base
5
+ include ::Musashi::Connection
6
+
7
+ attr_writer :path
8
+ def path
9
+ @path ||= connection.path_prefix
10
+ end
11
+
12
+ def retrieved?
13
+ @retrieved ||= false
14
+ end
15
+
16
+ def retrieve(header={})
17
+ response = connection.get(path, header)
18
+ @retrieved = true
19
+ response.body.extend(VisitorStrategist)
20
+ end
21
+
22
+ end
23
+
24
+ module Strategist
25
+
26
+ def self.extend_object(receiver)
27
+ self.define_by_behavior(receiver)
28
+ end
29
+
30
+ def self.retrievers_by_behavior
31
+ @@retrievers_by_behavior ||= {}
32
+ end
33
+
34
+ def self.retrievers_by_behavior=(retrievers)
35
+ @@retrievers_by_behavior = retrievers
36
+ end
37
+
38
+ def self.define_by_behavior(receiver)
39
+ @@retrievers_by_behavior.each do |retriever|
40
+ if retriever.extend?(receiver)
41
+ receiver.extend(retriever)
42
+ break
43
+ end
44
+ end
45
+ end
46
+
47
+ def self.retrievers_by_name
48
+ @@retrievers_by_name ||= {}
49
+ end
50
+
51
+ def self.retrievers_by_name=(retrievers)
52
+ @@retrievers_by_name = retrievers
53
+ end
54
+
55
+ def self.define_by_name(receiver,name)
56
+ raise "Undefined receiver" if receiver.nil?
57
+ raise "Undefined name for #{receiver}" if name.nil?
58
+ retriever = @@retrievers_by_name[name]
59
+ raise "Undefined retriever for #{receiver} with name=#{name}" if retriever.nil?
60
+ receiver.extend retriever
61
+ end
62
+
63
+ def define(receiver,name=nil)
64
+ raise "Undefined receiver" if receiver.nil?
65
+ if name.nil?
66
+ Strategist.define_by_behavior receiver
67
+ else
68
+ retriever = Strategist.retrievers_by_name[name]
69
+ if retriever.nil?
70
+ Strategist.define_by_behavior receiver
71
+ else
72
+ receiver.extend retriever
73
+ end
74
+ end
75
+ end
76
+
77
+ end
78
+
79
+ module VisitorStrategist
80
+ include Base
81
+ include Strategist
82
+
83
+ def self.extend?(receiver)
84
+ !receiver.nil? && receiver.respond_to?(:keys) && receiver.respond_to?(:fetch)
85
+ end
86
+
87
+ def self.extend_object(receiver)
88
+ raise RuntimeError.new unless extend? receiver
89
+ super
90
+ end
91
+
92
+ def [](key)
93
+ return nil unless self.has_key?(key)
94
+ value = self.fetch(key.to_s) rescue self.fetch(key) rescue nil
95
+ return value if value.nil?
96
+ define value,key.to_sym
97
+ value
98
+ end
99
+
100
+ def has_key?(key)
101
+ self.key?(key.to_s) || self.key?(key)
102
+ end
103
+
104
+ def respond_to?(method)
105
+ self.has_key?(method) ? true : super
106
+ end
107
+
108
+ def method_missing(sym,*args,&block)
109
+ super unless args.empty?
110
+ super unless has_key?(sym)
111
+
112
+ value = self.fetch(sym.to_s) rescue self.fetch(sym) rescue nil
113
+ return value if value.nil?
114
+
115
+ define value, sym
116
+ value
117
+ end
118
+
119
+ end
120
+
121
+ module DelegateHash
122
+ include VisitorStrategist
123
+
124
+ attr_accessor :attrs
125
+
126
+ def [](key)
127
+ @attrs = retrieve if @attrs.nil?
128
+ super
129
+ end
130
+
131
+ def method_missing(sym,*args,&block)
132
+ @attrs = retrieve if @attrs.nil?
133
+ super
134
+ end
135
+
136
+ def keys
137
+ @attrs = retrieve if @attrs.nil?
138
+ @attrs.keys
139
+ end
140
+
141
+ def key?(key)
142
+ @attrs = retrieve if @attrs.nil?
143
+ @attrs.key?(key)
144
+ end
145
+
146
+ def fetch(*args)
147
+ @attrs = retrieve if @attrs.nil?
148
+ @attrs.fetch(*args)
149
+ end
150
+
151
+ end
152
+
153
+ module Content
154
+ include DelegateHash
155
+
156
+ def self.extend?(receiver)
157
+ !receiver.nil? && receiver.respond_to?(:start_with?) && receiver.start_with?('http')
158
+ end
159
+
160
+ def self.extend_object(receiver)
161
+ raise RuntimeError.new unless extend? receiver
162
+ super
163
+ end
164
+
165
+ def retrieve
166
+ self.endpoint = to_s
167
+ super
168
+ end
169
+
170
+ end
171
+
172
+ module Iterator
173
+ def self.extend?(receiver)
174
+ !receiver.nil? && receiver.is_a?(::Array)
175
+ end
176
+
177
+ def self.extend_object(receiver)
178
+ raise RuntimeError.new unless extend? receiver
179
+ receiver.each do |item|
180
+ item.extend VisitorStrategist
181
+ end
182
+ end
183
+ end
184
+
185
+ Strategist.retrievers_by_behavior = [
186
+ Content,
187
+ VisitorStrategist,
188
+ Iterator
189
+ ]
190
+
191
+ include DelegateHash
192
+ end
@@ -0,0 +1,4 @@
1
+ # -*- encoding: utf-8 -*-
2
+ module Musashi
3
+ VERSION = "0.0.1.b1"
4
+ end
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require 'spec_helper'
3
+
4
+ describe Musashi::Resource do
5
+
6
+ before(:all) do
7
+ @customer = Musashi::Resource.new('http://localhost:4567/customers/1.json')
8
+ end
9
+
10
+ it 'should raise an error for undefined method' do
11
+ lambda{ @resource.foo }.should raise_error(NoMethodError)
12
+ end
13
+
14
+ it 'should retrieve a customer instance' do
15
+ @customer.id.should eq('1')
16
+ @customer.cpf.should eq('12345678912')
17
+ end
18
+
19
+ it 'should retrieve relationships' do
20
+ @customer.provisionings.entries.each{ |p| p.service_key.should_not be_nil }
21
+ end
22
+
23
+ end
24
+
@@ -0,0 +1,6 @@
1
+ # -*- enconding: utf-8 -*-
2
+ require 'sinatra'
3
+
4
+ public_dir = File.expand_path('../public/',__FILE__)
5
+ set :public, public_dir
6
+
@@ -0,0 +1,65 @@
1
+ {
2
+ "id": "1",
3
+ "login": "foo",
4
+ "name": "Foo Bar",
5
+ "cpf": "12345678912",
6
+ "rg": "12345678912",
7
+ "emails": [
8
+ "foo@gmail.com"
9
+ ],
10
+ "phones": [
11
+ "11111111",
12
+ "22222222"
13
+ ],
14
+ "address": {
15
+ "street": "No where",
16
+ "number": "42",
17
+ "complement": "apt-42",
18
+ "district": "Place",
19
+ "city": "Townsville",
20
+ "state": "Batman",
21
+ "country": "BR",
22
+ "postal_code": "12121212"
23
+ },
24
+ "twitter": "foo",
25
+ "facebook": "foo",
26
+ "link": [
27
+ {
28
+ "rel": "contacts",
29
+ "href": "http://localhost:4567/customers/1/contacts"
30
+ },
31
+ {
32
+ "rel": "billing_info",
33
+ "href": "http://localhost:4567/customers/1/billing_info"
34
+ },
35
+ {
36
+ "rel": "billing_contact",
37
+ "href": "http://localhost:4567/customers/1/billing_contact"
38
+ },
39
+ {
40
+ "rel": "balance",
41
+ "href": "http://localhost:4567/customers/1/balance"
42
+ },
43
+ {
44
+ "rel": "credit_limit",
45
+ "href": "http://localhost:4567/customers/1/credit_limit"
46
+ },
47
+ {
48
+ "rel": "manager",
49
+ "href": "http://localhost:4567/customers/1/manager"
50
+ },
51
+ {
52
+ "rel": "provisionings",
53
+ "href": "http://localhost:4567/customers/1/provisionings"
54
+ },
55
+ {
56
+ "rel": "orders",
57
+ "href": "http://localhost:4567/customers/1/orders"
58
+ },
59
+ {
60
+ "rel": "bills",
61
+ "href": "http://localhost:4567/customers/1/bills"
62
+ }
63
+ ]
64
+ }
65
+
File without changes
File without changes
@@ -0,0 +1 @@
1
+ {"entries":[{"id":"1234567890","parent":"0","service_key":"SERHOST0001","created_at":"2011-09-23","active":true,"status":"active"}]}
@@ -0,0 +1,4 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require 'rspec'
3
+ require 'musashi'
4
+
metadata ADDED
@@ -0,0 +1,181 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: musashi
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1.b1
5
+ prerelease: 6
6
+ platform: ruby
7
+ authors:
8
+ - pahagon
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-09-30 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: i18n
16
+ requirement: &8196940 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '0.6'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *8196940
25
+ - !ruby/object:Gem::Dependency
26
+ name: activesupport
27
+ requirement: &8196300 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: 3.1.0
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *8196300
36
+ - !ruby/object:Gem::Dependency
37
+ name: hashie
38
+ requirement: &8195460 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ version: 1.1.0
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *8195460
47
+ - !ruby/object:Gem::Dependency
48
+ name: faraday
49
+ requirement: &8194660 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 0.7.4
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: *8194660
58
+ - !ruby/object:Gem::Dependency
59
+ name: faraday_middleware
60
+ requirement: &8194040 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ~>
64
+ - !ruby/object:Gem::Version
65
+ version: 0.7.0
66
+ type: :runtime
67
+ prerelease: false
68
+ version_requirements: *8194040
69
+ - !ruby/object:Gem::Dependency
70
+ name: multi_json
71
+ requirement: &8193280 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ~>
75
+ - !ruby/object:Gem::Version
76
+ version: 1.0.0
77
+ type: :runtime
78
+ prerelease: false
79
+ version_requirements: *8193280
80
+ - !ruby/object:Gem::Dependency
81
+ name: multi_xml
82
+ requirement: &8192780 !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ~>
86
+ - !ruby/object:Gem::Version
87
+ version: 0.4.0
88
+ type: :runtime
89
+ prerelease: false
90
+ version_requirements: *8192780
91
+ - !ruby/object:Gem::Dependency
92
+ name: rspec
93
+ requirement: &8191840 !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - ! '>='
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ type: :development
100
+ prerelease: false
101
+ version_requirements: *8191840
102
+ - !ruby/object:Gem::Dependency
103
+ name: sinatra
104
+ requirement: &8188320 !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: *8188320
113
+ description: ! 'Musashi is a library for '
114
+ email: pahagon@gmail.com
115
+ executables: []
116
+ extensions: []
117
+ extra_rdoc_files: []
118
+ files:
119
+ - Rakefile
120
+ - LICENSE
121
+ - README.rdoc
122
+ - ROADMAP.md
123
+ - CHANGELOG.md
124
+ - lib/musashi/resource.rb
125
+ - lib/musashi/connection.rb
126
+ - lib/musashi/retriever.rb
127
+ - lib/musashi/version.rb
128
+ - lib/musashi.rb
129
+ - lib/faraday/response/raise_http_5xx.rb
130
+ - lib/faraday/response/raise_http_4xx.rb
131
+ - spec/spec_helper.rb
132
+ - spec/resource_spec.rb
133
+ - spec/sinatra/public/customers/1.json
134
+ - spec/sinatra/public/customers/1/orders
135
+ - spec/sinatra/public/customers/1/contacts
136
+ - spec/sinatra/public/customers/1/billing_info
137
+ - spec/sinatra/public/customers/1/provisionings
138
+ - spec/sinatra/public/customers/1/bills
139
+ - spec/sinatra/public/customers/1/balance
140
+ - spec/sinatra/public/customers/1/credit_limit
141
+ - spec/sinatra/public/customers/1/billing_contact
142
+ - spec/sinatra/public/customers/1/manager
143
+ - spec/sinatra/app.rb
144
+ homepage: http://github.com/pahagon/musashi
145
+ licenses: []
146
+ post_install_message:
147
+ rdoc_options: []
148
+ require_paths:
149
+ - lib
150
+ required_ruby_version: !ruby/object:Gem::Requirement
151
+ none: false
152
+ requirements:
153
+ - - ! '>='
154
+ - !ruby/object:Gem::Version
155
+ version: '0'
156
+ required_rubygems_version: !ruby/object:Gem::Requirement
157
+ none: false
158
+ requirements:
159
+ - - ! '>='
160
+ - !ruby/object:Gem::Version
161
+ version: 1.3.6
162
+ requirements: []
163
+ rubyforge_project: musashi
164
+ rubygems_version: 1.8.10
165
+ signing_key:
166
+ specification_version: 3
167
+ summary: Musashi is a library for
168
+ test_files:
169
+ - spec/spec_helper.rb
170
+ - spec/resource_spec.rb
171
+ - spec/sinatra/public/customers/1.json
172
+ - spec/sinatra/public/customers/1/orders
173
+ - spec/sinatra/public/customers/1/contacts
174
+ - spec/sinatra/public/customers/1/billing_info
175
+ - spec/sinatra/public/customers/1/provisionings
176
+ - spec/sinatra/public/customers/1/bills
177
+ - spec/sinatra/public/customers/1/balance
178
+ - spec/sinatra/public/customers/1/credit_limit
179
+ - spec/sinatra/public/customers/1/billing_contact
180
+ - spec/sinatra/public/customers/1/manager
181
+ - spec/sinatra/app.rb