roar 0.12.9 → 1.0.0.beta1

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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +8 -10
  4. data/CHANGES.markdown +24 -2
  5. data/Gemfile +5 -2
  6. data/README.markdown +132 -28
  7. data/TODO.markdown +9 -7
  8. data/examples/example.rb +2 -0
  9. data/examples/example_server.rb +19 -3
  10. data/gemfiles/Gemfile.representable-2.0 +5 -0
  11. data/gemfiles/Gemfile.representable-2.1 +5 -0
  12. data/lib/roar.rb +1 -1
  13. data/lib/roar/client.rb +38 -0
  14. data/lib/roar/{representer/feature/coercion.rb → coercion.rb} +2 -1
  15. data/lib/roar/decorator.rb +3 -11
  16. data/lib/roar/http_verbs.rb +88 -0
  17. data/lib/roar/hypermedia.rb +174 -0
  18. data/lib/roar/json.rb +55 -0
  19. data/lib/roar/json/collection.rb +3 -0
  20. data/lib/roar/{representer/json → json}/collection_json.rb +20 -20
  21. data/lib/roar/{representer/json → json}/hal.rb +33 -31
  22. data/lib/roar/json/hash.rb +3 -0
  23. data/lib/roar/json/json_api.rb +208 -0
  24. data/lib/roar/representer.rb +3 -36
  25. data/lib/roar/transport/faraday.rb +49 -0
  26. data/lib/roar/transport/net_http.rb +57 -0
  27. data/lib/roar/transport/net_http/request.rb +72 -0
  28. data/lib/roar/version.rb +1 -1
  29. data/lib/roar/xml.rb +54 -0
  30. data/roar.gemspec +5 -4
  31. data/test/client_test.rb +3 -3
  32. data/test/coercion_feature_test.rb +6 -3
  33. data/test/collection_json_test.rb +8 -10
  34. data/test/decorator_test.rb +27 -15
  35. data/test/faraday_http_transport_test.rb +13 -15
  36. data/test/hal_json_test.rb +16 -16
  37. data/test/hal_links_test.rb +3 -3
  38. data/test/http_verbs_test.rb +17 -22
  39. data/test/hypermedia_feature_test.rb +23 -45
  40. data/test/hypermedia_test.rb +11 -23
  41. data/test/integration/band_representer.rb +2 -2
  42. data/test/integration/runner.rb +4 -3
  43. data/test/integration/server.rb +13 -2
  44. data/test/integration/ssl_server.rb +1 -1
  45. data/test/json_api_test.rb +336 -0
  46. data/test/json_representer_test.rb +16 -12
  47. data/test/lib/runner.rb +134 -0
  48. data/test/lonely_test.rb +9 -0
  49. data/test/net_http_transport_test.rb +4 -4
  50. data/test/representer_test.rb +2 -2
  51. data/test/{lib/roar/representer/transport/net_http/request_test.rb → ssl_client_certs_test.rb} +43 -5
  52. data/test/test_helper.rb +12 -5
  53. data/test/xml_representer_test.rb +26 -166
  54. metadata +49 -29
  55. data/gemfiles/Gemfile.representable-1.7 +0 -6
  56. data/gemfiles/Gemfile.representable-1.8 +0 -6
  57. data/lib/roar/representer/feature/client.rb +0 -39
  58. data/lib/roar/representer/feature/http_verbs.rb +0 -95
  59. data/lib/roar/representer/feature/hypermedia.rb +0 -175
  60. data/lib/roar/representer/json.rb +0 -67
  61. data/lib/roar/representer/transport/faraday.rb +0 -50
  62. data/lib/roar/representer/transport/net_http.rb +0 -59
  63. data/lib/roar/representer/transport/net_http/request.rb +0 -75
  64. data/lib/roar/representer/xml.rb +0 -61
@@ -0,0 +1,57 @@
1
+ require 'roar/transport/net_http/request'
2
+
3
+ module Roar
4
+ # Implements the (HTTP) transport interface with Net::HTTP.
5
+ module Transport
6
+ # Low-level interface for HTTP. The #get_uri and friends accept an options and an optional block, invoke
7
+ # the HTTP request and return the request object.
8
+ #
9
+ # The following options are available:
10
+ class NetHTTP
11
+
12
+ def get_uri(*options, &block)
13
+ call(Net::HTTP::Get, *options, &block)
14
+ end
15
+
16
+ def post_uri(*options, &block)
17
+ call(Net::HTTP::Post, *options, &block)
18
+ end
19
+
20
+ def put_uri(*options, &block)
21
+ call(Net::HTTP::Put, *options, &block)
22
+ end
23
+
24
+ def delete_uri(*options, &block)
25
+ call(Net::HTTP::Delete, *options, &block)
26
+ end
27
+
28
+ def patch_uri(*options, &block)
29
+ call(Net::HTTP::Patch, *options, &block)
30
+ end
31
+
32
+ private
33
+ def call(what, *args, &block)
34
+ options = handle_deprecated_args(args)
35
+ # TODO: generically handle return codes.
36
+ Request.new(options).call(what, &block)
37
+ end
38
+
39
+ def handle_deprecated_args(args) # TODO: remove in 1.0.
40
+ if args.size > 1
41
+ warn %{DEPRECATION WARNING: #get_uri, #post_uri, #put_uri, #delete_uri and #patch_uri no longer accept positional arguments. Please call them as follows:
42
+ get_uri(uri: "http://localhost/songs", as: "application/json")
43
+ post_uri(uri: "http://localhost/songs", as: "application/json", body: "{'id': 1}")
44
+ Thank you and have a lovely day.}
45
+ return {:uri => args[0], :as => args[1]} if args.size == 2
46
+ return {:uri => args[0], :as => args[2], :body => args[1]}
47
+ end
48
+
49
+ args.first
50
+ end
51
+
52
+ end
53
+
54
+ class UnauthorizedError < RuntimeError # TODO: raise this from Faraday, too.
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,72 @@
1
+ require "net/http"
2
+ require "openssl"
3
+
4
+ module Roar
5
+ module Transport
6
+ class NetHTTP
7
+ class Request # TODO: implement me.
8
+ def initialize(options)
9
+ @uri = parse_uri(options[:uri]) # TODO: add :uri.
10
+ @as = options[:as]
11
+ @body = options[:body]
12
+ @options = options
13
+
14
+ @http = Net::HTTP.new(uri.host, uri.port)
15
+ unless options[:pem_file].nil?
16
+ pem = File.read(options[:pem_file])
17
+ @http.use_ssl = true
18
+ @http.cert = OpenSSL::X509::Certificate.new(pem)
19
+ @http.key = OpenSSL::PKey::RSA.new(pem)
20
+ @http.verify_mode = options[:ssl_verify_mode].nil? ? OpenSSL::SSL::VERIFY_PEER : options[:ssl_verify_mode]
21
+ end
22
+ end
23
+
24
+ def call(what)
25
+ @req = what.new(uri.request_uri)
26
+
27
+ # if options[:ssl]
28
+ # uri.port = Net::HTTP.https_default_port()
29
+ # end
30
+ https!
31
+ basic_auth!
32
+
33
+ req.content_type = as
34
+ req["accept"] = as # TODO: test me. # DISCUSS: if Accept is not set, rails treats this request as as "text/html".
35
+ req.body = body if body
36
+
37
+ yield req if block_given?
38
+
39
+ http.request(req).tap do |res|
40
+ raise UnauthorizedError if res.is_a?(Net::HTTPUnauthorized) # FIXME: make this better. # DISCUSS: abstract all that crap here?
41
+ end
42
+ end
43
+
44
+ def get
45
+ call(Net::HTTP::Get)
46
+ end
47
+
48
+ private
49
+ attr_reader :uri, :as, :body, :options, :req, :http
50
+
51
+ def parse_uri(url)
52
+ uri = URI(url)
53
+ raise "Incorrect URL `#{url}`. Maybe you forgot http://?" if uri.instance_of?(URI::Generic)
54
+ uri
55
+ end
56
+
57
+ def https!
58
+ return unless uri.scheme == 'https'
59
+
60
+ @http.use_ssl = true
61
+ @http.verify_mode = OpenSSL::SSL::VERIFY_NONE
62
+ end
63
+
64
+ def basic_auth!
65
+ return unless options[:basic_auth]
66
+
67
+ @req.basic_auth(*options[:basic_auth])
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
data/lib/roar/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Roar
2
- VERSION = "0.12.9"
2
+ VERSION = "1.0.0.beta1"
3
3
  end
data/lib/roar/xml.rb ADDED
@@ -0,0 +1,54 @@
1
+ require 'roar/representer'
2
+ require 'roar/hypermedia'
3
+ require 'representable/xml'
4
+
5
+ module Roar
6
+ # Includes #from_xml and #to_xml into your represented object.
7
+ # In addition to that, some more options are available when declaring properties.
8
+ module XML
9
+ def self.included(base)
10
+ base.class_eval do
11
+ include Representer # Roar::Representer, this is needed for Rails URL helpers
12
+ include Representable::XML
13
+
14
+ extend ClassMethods
15
+ include InstanceMethods # otherwise Representable overrides our #to_xml.
16
+ end
17
+ end
18
+
19
+ module InstanceMethods
20
+ # Generic entry-point for rendering.
21
+ def serialize(*args)
22
+ to_xml(*args)
23
+ end
24
+
25
+ def deserialize(*args)
26
+ from_xml(*args)
27
+ end
28
+ end
29
+
30
+
31
+ module ClassMethods
32
+ include Representable::XML::ClassMethods
33
+
34
+ def links_definition_options
35
+ # FIXME: this doesn't belong into the generic XML representer.
36
+ {
37
+ :as => :link,
38
+ :collection => true,
39
+ :class => Hypermedia::Hyperlink,
40
+ :extend => XML::HyperlinkRepresenter,
41
+ :exec_context => :decorator,
42
+ } # TODO: merge with JSON.
43
+ end
44
+ end
45
+
46
+
47
+ require 'representable/xml/hash'
48
+ module HyperlinkRepresenter
49
+ include Representable::XML::AttributeHash
50
+
51
+ self.representation_wrap = :link
52
+ end
53
+ end
54
+ end
data/roar.gemspec CHANGED
@@ -13,17 +13,18 @@ Gem::Specification.new do |s|
13
13
  s.license = 'MIT'
14
14
 
15
15
  s.files = `git ls-files`.split("\n")
16
- s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+ s.test_files = `git ls-files -- {test}/*`.split("\n")
17
17
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
18
  s.require_paths = ["lib"]
19
19
 
20
- s.add_runtime_dependency "representable", ">= 1.6.0", "< 2.0.0"
20
+ s.add_runtime_dependency "representable", ">= 2.0.0", "<= 3.0.0"
21
21
 
22
22
  s.add_development_dependency "rake", ">= 0.10.1"
23
23
  s.add_development_dependency "test_xml", ">= 0.1.6"
24
- s.add_development_dependency "minitest", "= 5.0.0"
24
+ s.add_development_dependency "minitest", ">= 5.4.2"
25
25
  s.add_development_dependency "sinatra"
26
- s.add_development_dependency "virtus"
26
+ s.add_development_dependency "sinatra-contrib"
27
+ s.add_development_dependency "virtus", ">= 1.0.0"
27
28
  s.add_development_dependency "faraday"
28
29
  s.add_development_dependency "json"
29
30
  end
data/test/client_test.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  require 'test_helper'
2
- require 'roar/representer/feature/client'
2
+ require 'roar/client'
3
3
 
4
4
  class ClientTest < MiniTest::Spec
5
5
  representer_for([Roar::Representer]) do
@@ -7,7 +7,7 @@ class ClientTest < MiniTest::Spec
7
7
  property :band
8
8
  end
9
9
 
10
- let(:song) { Object.new.extend(rpr).extend(Roar::Representer::Feature::Client) }
10
+ let(:song) { Object.new.extend(rpr).extend(Roar::Client) }
11
11
 
12
12
  it "adds accessors" do
13
13
  song.name = "Social Suicide"
@@ -17,7 +17,7 @@ class ClientTest < MiniTest::Spec
17
17
  end
18
18
 
19
19
  describe "links" do
20
- representer_for([Roar::Representer::JSON, Roar::Representer::Feature::Hypermedia]) do
20
+ representer_for([Roar::JSON, Roar::Hypermedia]) do
21
21
  property :name
22
22
  link(:self) { never_call_me! }
23
23
  end
@@ -1,15 +1,18 @@
1
1
  require 'test_helper'
2
- require 'roar/representer/feature/coercion'
2
+ require 'roar/coercion'
3
3
 
4
4
  class CoercionFeatureTest < MiniTest::Spec
5
5
  describe "Coercion" do
6
6
  class ImmigrantSong
7
- include Roar::Representer::JSON
8
- include Roar::Representer::Feature::Coercion
7
+ include Roar::JSON
8
+ include Roar::Coercion
9
9
 
10
10
  property :composed_at, :type => DateTime, :default => "May 12th, 2012"
11
11
 
12
12
  attr_accessor :composed_at
13
+ def composed_at=(v) # in ruby 2.2, #label= is not there, all at sudden. what *is* that?
14
+ @composed_at = v
15
+ end
13
16
  end
14
17
 
15
18
  it "coerces into the provided type" do
@@ -1,10 +1,10 @@
1
1
  require 'test_helper'
2
- require 'roar/representer/json/collection_json'
2
+ require 'roar/json/collection_json'
3
3
 
4
4
  class CollectionJsonTest < MiniTest::Spec
5
5
  let(:song) { OpenStruct.new(:title => "scarifice", :length => 43) }
6
6
 
7
- representer_for([Roar::Representer::JSON::CollectionJSON]) do
7
+ representer_for([Roar::JSON::CollectionJSON]) do
8
8
  version "1.0"
9
9
  href { "//songs/" }
10
10
 
@@ -34,11 +34,9 @@ class CollectionJsonTest < MiniTest::Spec
34
34
 
35
35
  describe "#to_json" do
36
36
  it "renders document" do
37
- collection_key = Representable::VERSION =~ /^1.8/ ? "collection" : :collection
38
-
39
37
  [song].extend(rpr).to_hash.must_equal(
40
38
  {
41
- collection_key=>{
39
+ "collection"=>{
42
40
  "version"=>"1.0",
43
41
  "href"=>"//songs/",
44
42
 
@@ -50,8 +48,8 @@ class CollectionJsonTest < MiniTest::Spec
50
48
  },
51
49
 
52
50
  "queries"=>[
53
- {:rel=>:search, :href=>"//search",
54
- :data=>[
51
+ {"rel"=>"search", "href"=>"//search",
52
+ "data"=>[
55
53
  {:name=>"q", :value=>""}
56
54
  ]
57
55
  }
@@ -60,8 +58,8 @@ class CollectionJsonTest < MiniTest::Spec
60
58
  "items"=>[
61
59
  {
62
60
  "links"=>[
63
- {:rel=>:download, :href=>"//songs/scarifice.mp3"},
64
- {:rel=>:stats, :href=>"//songs/scarifice/stats"}
61
+ {"rel"=>"download", "href"=>"//songs/scarifice.mp3"},
62
+ {"rel"=>"stats", "href"=>"//songs/scarifice/stats"}
65
63
  ],
66
64
  "href"=>"//songs/scarifice",
67
65
  :data=>[
@@ -72,7 +70,7 @@ class CollectionJsonTest < MiniTest::Spec
72
70
  ],
73
71
 
74
72
  "links"=>[
75
- {:rel=>:feed, :href=>"//songs/feed"}
73
+ {"rel"=>"feed", "href"=>"//songs/feed"}
76
74
  ]
77
75
  }
78
76
  })# %{{"collection":{"version":"1.0","href":"//songs/","items":[{"href":"//songs/scarifice","links":[{"rel":"download","href":"//songs/scarifice.mp3"},{"rel":"stats","href":"//songs/scarifice/stats"}],"data":[{"name":"title","value":"scarifice"},{"name":"length","value":43}]}],"template":{"data":[{"name":"title","value":null},{"name":"length","value":null}]},"queries":[{"rel":"search","href":"//search","data":[{"name":"q","value":""}]}],"links":[{"rel":"feed","href":"//songs/feed"}]}}}
@@ -3,7 +3,7 @@ require 'roar/decorator'
3
3
 
4
4
  class DecoratorTest < MiniTest::Spec
5
5
  class SongRepresentation < Roar::Decorator
6
- include Roar::Representer::JSON
6
+ include Roar::JSON
7
7
 
8
8
  property :name
9
9
  end
@@ -32,7 +32,7 @@ class DecoratorTest < MiniTest::Spec
32
32
  let (:decorator) { decorator_class.new(model) }
33
33
 
34
34
  it "rendering links works" do
35
- decorator.to_hash.must_equal({"links"=>[{:rel=>:self, :href=>"http://self"}]})
35
+ decorator.to_hash.must_equal({"links"=>[{"rel"=>"self", "href"=>"http://self"}]})
36
36
  end
37
37
 
38
38
  it "sets links on decorator" do
@@ -45,22 +45,34 @@ class DecoratorTest < MiniTest::Spec
45
45
  model_with_links.links.must_equal nil
46
46
  end
47
47
 
48
- describe "Decorator::HypermediaClient" do
49
- let (:decorator) { rpr_mod = rpr
50
- Class.new(Roar::Decorator) do
51
- include rpr_mod
52
- include Roar::Decorator::HypermediaConsumer
53
- end }
48
+ class ConsumingDecorator < Roar::Decorator
49
+ include Roar::JSON
50
+ include Roar::Hypermedia
51
+ link(:self) { "http://self" }
52
+
53
+ include HypermediaConsumer
54
+ end
55
+
56
+ # TODO: test include ModuleWithLinks
54
57
 
58
+ describe "Decorator::HypermediaClient" do
55
59
  it "propagates links to represented" do
56
- decorator.new(model_with_links).from_hash("links"=>[{:rel=>:self, :href=>"http://next"}])
57
- model_with_links.links[:self].must_equal(link(:rel=>:self, :href=>"http://next"))
60
+ decorator = ConsumingDecorator.new(model_with_links)
61
+
62
+
63
+ decorator.from_hash("links"=>[{:rel=>:self, :href=>"http://percolator"}])
64
+
65
+ # links are always set on decorator instance.
66
+ decorator .links[:self].must_equal(link(:rel=>:self, :href=>"http://percolator"))
67
+
68
+ # and propagated to represented with HypermediaConsumer.
69
+ model_with_links.links[:self].must_equal(link(:rel=>:self, :href=>"http://percolator"))
58
70
  end
59
71
  end
60
72
  end
61
73
 
62
74
  describe "XML" do
63
- representer_for([Roar::Representer::XML, Roar::Representer::Feature::Hypermedia]) do
75
+ representer_for([Roar::XML, Roar::Hypermedia]) do
64
76
  link(:self) { "http://self" } # TODO: test with HAL, too.
65
77
  #self.representation_wrap = :song # FIXME: why isn't this working?
66
78
  end
@@ -84,7 +96,7 @@ class DecoratorTest < MiniTest::Spec
84
96
 
85
97
 
86
98
  describe "JSON::HAL" do
87
- representer_for([Roar::Representer::JSON::HAL]) do
99
+ representer_for([Roar::JSON::HAL]) do
88
100
  link(:self) { "http://self" }
89
101
  end
90
102
  let (:decorator_class) { rpr_mod = rpr
@@ -95,12 +107,12 @@ class DecoratorTest < MiniTest::Spec
95
107
  let (:decorator) { decorator_class.new(model) }
96
108
 
97
109
  it "rendering links works" do
98
- decorator.to_hash.must_equal({"_links"=>{"self"=>{:href=>"http://self"}}})
110
+ decorator.to_hash.must_equal({"_links"=>{"self"=>{"href"=>"http://self"}}})
99
111
  end
100
112
 
101
113
  it "sets links on decorator" do
102
- decorator.from_hash({"_links"=>{"self"=>{:href=>"http://next"}}})
103
- decorator.links.must_equal("self"=>link(:rel=>"self", :href=>"http://next"))
114
+ decorator.from_hash({"_links"=>{"self"=>{"href"=>"http://next"}}})
115
+ decorator.links.must_equal("self"=>link("rel"=>"self", "href"=>"http://next"))
104
116
  end
105
117
 
106
118
  describe "Decorator::HypermediaClient" do
@@ -1,5 +1,5 @@
1
1
  require 'test_helper'
2
- require 'roar/representer/transport/faraday'
2
+ require 'roar/transport/faraday'
3
3
 
4
4
  class FaradayHttpTransportTest < MiniTest::Spec
5
5
  describe 'FaradayHttpTransport' do
@@ -7,55 +7,53 @@ class FaradayHttpTransportTest < MiniTest::Spec
7
7
  let(:body) { "booty" }
8
8
  let(:as) { "application/xml" }
9
9
  before do
10
- @transport = Roar::Representer::Transport::Faraday.new
10
+ @transport = Roar::Transport::Faraday.new
11
11
  end
12
12
 
13
13
  it "#get_uri returns response" do
14
- @transport.get_uri(url, as).must_match_faraday_response :get, url, as
14
+ @transport.get_uri(uri: url, as: as).must_match_faraday_response :get, url, as
15
15
  end
16
16
 
17
17
  it "#post_uri returns response" do
18
- @transport.post_uri(url, body, as).must_match_faraday_response :post, url, as, body
18
+ @transport.post_uri(uri: url, body: body, as: as).must_match_faraday_response :post, url, as, body
19
19
  end
20
20
 
21
21
  it "#put_uri returns response" do
22
- @transport.put_uri(url, body, as).must_match_faraday_response :put, url, as, body
22
+ @transport.put_uri(uri: url, body: body, as: as).must_match_faraday_response :put, url, as, body
23
23
  end
24
24
 
25
25
  it "#delete_uri returns response" do
26
- @transport.delete_uri(url, as).must_match_faraday_response :delete, url, as
26
+ @transport.delete_uri(uri: url, as: as).must_match_faraday_response :delete, url, as
27
27
  end
28
28
 
29
29
  it "#patch_uri returns response" do
30
- @transport.patch_uri(url, body, as).must_match_faraday_response :patch, url, as, body
30
+ @transport.patch_uri(uri: url, body: body, as: as).must_match_faraday_response :patch, url, as, body
31
31
  end
32
32
 
33
33
  describe 'non-existent resource' do
34
- before do
35
- @not_found_url = 'http://localhost:4567/missing-resource'
36
- end
34
+ let(:not_found_url) { 'http://localhost:4567/missing-resource' }
37
35
 
38
36
  it '#get_uri raises a ResourceNotFound error' do
39
37
  assert_raises(Faraday::Error::ResourceNotFound) do
40
- @transport.get_uri(@not_found_url, as).body
38
+ @transport.get_uri(uri: not_found_url, as: as).body
41
39
  end
42
40
  end
43
41
 
44
42
  it '#post_uri raises a ResourceNotFound error' do
45
43
  assert_raises(Faraday::Error::ResourceNotFound) do
46
- @transport.post_uri(@not_found_url, body, as).body
44
+ @transport.post_uri(uri: not_found_url, body: body, as: as).body
47
45
  end
48
46
  end
49
47
 
50
48
  it '#post_uri raises a ResourceNotFound error' do
51
49
  assert_raises(Faraday::Error::ResourceNotFound) do
52
- @transport.post_uri(@not_found_url, body, as).body
50
+ @transport.post_uri(uri: not_found_url, body: body, as: as).body
53
51
  end
54
52
  end
55
53
 
56
54
  it '#delete_uri raises a ResourceNotFound error' do
57
55
  assert_raises(Faraday::Error::ResourceNotFound) do
58
- @transport.delete_uri(@not_found_url, as).body
56
+ @transport.delete_uri(uri: not_found_url, body: body, as: as).body
59
57
  end
60
58
  end
61
59
  end
@@ -63,7 +61,7 @@ class FaradayHttpTransportTest < MiniTest::Spec
63
61
  describe 'server errors (500 Internal Server Error)' do
64
62
  it '#get_uri raises a ClientError' do
65
63
  assert_raises(Faraday::Error::ClientError) do
66
- @transport.get_uri('http://localhost:4567/deliberate-error', as).body
64
+ @transport.get_uri(uri: 'http://localhost:4567/deliberate-error', as: as).body
67
65
  end
68
66
  end
69
67
  end