rest-core 3.2.0 → 3.3.0

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +23 -0
  3. data/Gemfile +1 -1
  4. data/README.md +11 -10
  5. data/Rakefile +2 -1
  6. data/lib/rest-core.rb +0 -1
  7. data/lib/rest-core/builder.rb +59 -12
  8. data/lib/rest-core/client/universal.rb +11 -10
  9. data/lib/rest-core/middleware.rb +20 -0
  10. data/lib/rest-core/middleware/cache.rb +12 -10
  11. data/lib/rest-core/middleware/default_headers.rb +2 -2
  12. data/lib/rest-core/middleware/default_payload.rb +2 -26
  13. data/lib/rest-core/middleware/default_query.rb +2 -10
  14. data/lib/rest-core/middleware/json_request.rb +2 -1
  15. data/lib/rest-core/middleware/json_response.rb +5 -2
  16. data/lib/rest-core/test.rb +3 -12
  17. data/lib/rest-core/version.rb +1 -1
  18. data/rest-core.gemspec +11 -12
  19. data/task/gemgem.rb +1 -5
  20. data/test/test_auth_basic.rb +4 -4
  21. data/test/test_builder.rb +20 -4
  22. data/test/test_cache.rb +19 -20
  23. data/test/test_clash.rb +1 -1
  24. data/test/test_clash_response.rb +11 -11
  25. data/test/test_client.rb +10 -10
  26. data/test/test_client_oauth1.rb +3 -3
  27. data/test/test_config.rb +1 -1
  28. data/test/test_default_headers.rb +13 -0
  29. data/test/test_default_payload.rb +11 -3
  30. data/test/test_default_query.rb +12 -4
  31. data/test/test_error_detector.rb +1 -1
  32. data/test/test_error_detector_http.rb +1 -1
  33. data/test/test_error_handler.rb +5 -5
  34. data/test/test_event_source.rb +16 -16
  35. data/test/test_follow_redirect.rb +6 -6
  36. data/test/test_future.rb +2 -2
  37. data/test/test_json_request.rb +9 -4
  38. data/test/test_json_response.rb +9 -9
  39. data/test/test_oauth1_header.rb +9 -9
  40. data/test/test_oauth2_header.rb +3 -3
  41. data/test/test_parse_link.rb +4 -4
  42. data/test/test_payload.rb +21 -21
  43. data/test/test_promise.rb +7 -7
  44. data/test/test_query_response.rb +5 -5
  45. data/test/test_rest-client.rb +7 -6
  46. data/test/test_simple.rb +5 -5
  47. data/test/test_smash.rb +1 -1
  48. data/test/test_smash_response.rb +11 -11
  49. data/test/test_thread_pool.rb +1 -1
  50. data/test/test_timeout.rb +3 -3
  51. data/test/test_universal.rb +12 -2
  52. metadata +9 -10
  53. data/lib/rest-core/wrapper.rb +0 -72
  54. data/test/test_wrapper.rb +0 -36
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: fbe0fe23ff10ec39cf8a2b367616b1c332bd0d42
4
- data.tar.gz: 0a7132a6620ecab1c35fd69fdff24733161aafad
3
+ metadata.gz: 2aa111679bf7c7b75c113cea9c30e4c52c709ed0
4
+ data.tar.gz: fe0dda1aea907ef4141f7125a7dfb9a881d42441
5
5
  SHA512:
6
- metadata.gz: d59288134f72ed58c335e743e439479d93de99f6c4278d871733ec845790d65cede5986e276f0a8cd0f0aa39f1cf2ee38325ef468e74cbcb4a08bf3cb01ed2cc
7
- data.tar.gz: 73a39d9b95793cc496d05e6c9370e0da5bd7c3ae22374e3ebe4ebc71d3dfd0d2778f885a9b8395fe1c8d38fd143b767df45d57ccbfcb6ae957116a8139ba2c41
6
+ metadata.gz: 4e634ba680b5cca9604ea54a5182e0afe161bf44e80b1f7748d8ffc922429023fb99fd009aea9a3c4b001b5531e9de315947de3db6229020de30c2bc88b8d9c7
7
+ data.tar.gz: 1a110e340a1f5e64e5ede750e6bca7a9ef38fa2137d27c53e900be9026b0d8d8f61664e7fc7032954bcb975ea45a6eea221e74ea7e01c9a62b3ca89c19f0b225
data/CHANGES.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # CHANGES
2
2
 
3
+ ## rest-core 3.3.0 -- 2014-08-25
4
+
5
+ ### Incompatible changes
6
+
7
+ * Removed `RC::Wrapper`. Apparently it's introducing more troubles than the
8
+ benefit than it brings. Currently, only `RC::Cache` is really using it,
9
+ and now the old functionality is merged back to `RC::Builder`.
10
+
11
+ * Therefore `RC::Cache` is no longer accepting a block.
12
+
13
+ * `RC::Universal` is then updated accordingly to respect the new `RC::Cache`.
14
+
15
+ ### Enhancements
16
+
17
+ * Now `RC::DefaultQuery`, `RC::DefaultPayload`, and `RC::DefaultHeaders`
18
+ work the same way. Previously they merge hashes slightly differently.
19
+
20
+ * Introduced `RC::Middleware#member=` in addition to `RC::Middleware#member`.
21
+
22
+ * RC::JsonResponse would now strip the problematic UTF-8 BOM before parsing.
23
+ This was introduced because Stackoverflow would return it. We also
24
+ try to not raise any encoding issues here.
25
+
3
26
  ## rest-core 3.2.0 -- 2014-06-27
4
27
 
5
28
  ### Enhancements
data/Gemfile CHANGED
@@ -6,7 +6,7 @@ gemspec
6
6
  gem 'rest-client'
7
7
 
8
8
  gem 'rake'
9
- gem 'bacon'
9
+ gem 'pork'
10
10
  gem 'muack'
11
11
  gem 'webmock'
12
12
 
data/README.md CHANGED
@@ -409,18 +409,19 @@ module RestCore
409
409
  use DefaultPayload, {}
410
410
  use JsonRequest , false
411
411
  use AuthBasic , nil, nil
412
+ use CommonLogger , method(:puts)
413
+ use ErrorHandler , nil
414
+ use ErrorDetectorHttp
415
+
416
+ use SmashResponse , false
417
+ use ClashResponse , false
418
+ use JsonResponse , false
419
+ use QueryResponse , false
420
+
421
+ use Cache , {}, 600 # default :expires_in 600 but the default
422
+ # cache {} didn't support it
412
423
 
413
424
  use FollowRedirect, 10
414
- use CommonLogger , method(:puts)
415
- use Cache , {}, 600 do # default :expires_in 600 but the default
416
- # cache {} didn't support it
417
- use ErrorHandler, nil
418
- use ErrorDetectorHttp
419
- use SmashResponse, false
420
- use ClashResponse, false
421
- use JsonResponse, false
422
- use QueryResponse, false
423
- end
424
425
  end
425
426
  end
426
427
  ```
data/Rakefile CHANGED
@@ -11,5 +11,6 @@ Gemgem.init(dir) do |s|
11
11
  s.name = 'rest-core'
12
12
  s.version = RestCore::VERSION
13
13
  s.homepage = 'https://github.com/godfat/rest-core'
14
- %w[httpclient mime-types timers].each{ |g| s.add_runtime_dependency(g) }
14
+ %w[httpclient mime-types].each{ |g| s.add_runtime_dependency(g) }
15
+ s.add_runtime_dependency('timers', '<4')
15
16
  end
data/lib/rest-core.rb CHANGED
@@ -29,7 +29,6 @@ module RestCore
29
29
  autoload :Error , 'rest-core/error'
30
30
  autoload :Event , 'rest-core/event'
31
31
  autoload :Middleware , 'rest-core/middleware'
32
- autoload :Wrapper , 'rest-core/wrapper'
33
32
  autoload :Promise , 'rest-core/promise'
34
33
  autoload :ThreadPool , 'rest-core/thread_pool'
35
34
  autoload :EventSource , 'rest-core/event_source'
@@ -1,18 +1,61 @@
1
1
 
2
2
  require 'thread'
3
3
  require 'rest-core/client'
4
- require 'rest-core/wrapper'
5
4
 
6
5
  class RestCore::Builder
7
6
  include RestCore
8
- include Wrapper
9
7
 
10
- def self.default_engine
11
- @default_engine ||= RestCore::HttpClient
8
+ singleton_class.module_eval do
9
+ attr_writer :default_engine
10
+ def default_engine
11
+ @default_engine ||= RestCore::HttpClient
12
+ end
13
+
14
+ def client *attrs, &block
15
+ new(&block).to_client(*attrs)
16
+ end
17
+ end
18
+
19
+ def initialize &block
20
+ @engine = nil
21
+ @middles ||= []
22
+ instance_eval(&block) if block_given?
23
+ end
24
+
25
+ attr_reader :middles
26
+ attr_writer :default_engine
27
+ def default_engine
28
+ @default_engine ||= self.class.default_engine
12
29
  end
13
30
 
14
- def self.client *attrs, &block
15
- new(&block).to_client(*attrs)
31
+ def use middle, *args, &block
32
+ middles << [middle, args, block]
33
+ end
34
+
35
+ def run engine
36
+ @engine = engine
37
+ end
38
+
39
+ def members
40
+ middles.map{ |(middle, args, block)|
41
+ if middle.public_method_defined?(:wrapped)
42
+ # TODO: this is hacky... try to avoid calling new!
43
+ middle.members + middle.new(Dry.new, *args, &block).members
44
+ else
45
+ middle.members
46
+ end if middle.respond_to?(:members)
47
+ }.flatten.compact
48
+ end
49
+
50
+ def to_app engine=@engine || default_engine
51
+ # === foldr m.new app middles
52
+ middles.reverse.inject(engine.new){ |app, (middle, args, block)|
53
+ begin
54
+ middle.new(app, *partial_deep_copy(args), &block)
55
+ rescue ArgumentError => e
56
+ raise ArgumentError.new("#{middle}: #{e}")
57
+ end
58
+ }
16
59
  end
17
60
 
18
61
  def to_client *attrs
@@ -33,6 +76,16 @@ class RestCore::Builder
33
76
  client
34
77
  end
35
78
 
79
+ private
80
+ def partial_deep_copy obj
81
+ case obj
82
+ when Array; obj.map{ |o| partial_deep_copy(o) }
83
+ when Hash ; obj.inject({}){ |r, (k, v)| r[k] = partial_deep_copy(v); r }
84
+ when Numeric, Symbol, TrueClass, FalseClass, NilClass; obj
85
+ else begin obj.dup; rescue TypeError; obj; end
86
+ end
87
+ end
88
+
36
89
  def build_struct fields
37
90
  if fields.empty?
38
91
  Struct.new(nil)
@@ -89,10 +142,4 @@ class RestCore::Builder
89
142
  end
90
143
  end
91
144
  end
92
-
93
- def initialize &block
94
- @engine = nil
95
- @middles ||= []
96
- instance_eval(&block) if block_given?
97
- end
98
145
  end
@@ -9,17 +9,18 @@ module RestCore
9
9
  use DefaultPayload, {}
10
10
  use JsonRequest , false
11
11
  use AuthBasic , nil, nil
12
+ use CommonLogger , method(:puts)
13
+ use ErrorHandler , nil
14
+ use ErrorDetectorHttp
15
+
16
+ use SmashResponse , false
17
+ use ClashResponse , false
18
+ use JsonResponse , false
19
+ use QueryResponse , false
20
+
21
+ use Cache , {}, 600 # default :expires_in 600 but the default
22
+ # cache {} didn't support it
12
23
 
13
24
  use FollowRedirect, 10
14
- use CommonLogger , method(:puts)
15
- use Cache , {}, 600 do # default :expires_in 600 but the default
16
- # cache {} didn't support it
17
- use ErrorHandler, nil
18
- use ErrorDetectorHttp
19
- use SmashResponse, false
20
- use ClashResponse, false
21
- use JsonResponse, false
22
- use QueryResponse, false
23
- end
24
25
  end
25
26
  end
@@ -10,6 +10,7 @@ module RestCore::Middleware
10
10
  mod.send(:attr_reader, :app)
11
11
  mem = if mod.respond_to?(:members) then mod.members else [] end
12
12
  src = mem.map{ |member| <<-RUBY }
13
+ attr_writer :#{member}
13
14
  def #{member} env
14
15
  if env.key?('#{member}')
15
16
  env['#{member}']
@@ -113,4 +114,23 @@ module RestCore::Middleware
113
114
  }
114
115
  end
115
116
  public :string_keys
117
+
118
+ # this method is intended to merge payloads if they are non-empty hashes,
119
+ # but prefer the right most one if they are not hashes.
120
+ def merge_hash *hashes
121
+ hashes.reverse_each.inject do |r, i|
122
+ if r.kind_of?(Hash)
123
+ if i.kind_of?(Hash)
124
+ Middleware.string_keys(i).merge(Middleware.string_keys(r))
125
+ elsif r.empty?
126
+ i # prefer non-empty ones
127
+ else
128
+ r # don't try to merge non-hashes
129
+ end
130
+ else
131
+ r
132
+ end
133
+ end
134
+ end
135
+ public :merge_hash
116
136
  end
@@ -1,14 +1,12 @@
1
1
 
2
2
  require 'rest-core/event'
3
3
  require 'rest-core/middleware'
4
- require 'rest-core/wrapper'
5
4
 
6
5
  require 'digest/md5'
7
6
 
8
7
  class RestCore::Cache
9
8
  def self.members; [:cache, :expires_in]; end
10
9
  include RestCore::Middleware
11
- include RestCore::Wrapper
12
10
 
13
11
  def initialize app, cache, expires_in, &block
14
12
  super(&block)
@@ -24,14 +22,18 @@ class RestCore::Cache
24
22
 
25
23
  cache_get(e){ |cached|
26
24
  e[TIMER].cancel if e[TIMER]
27
- wrapped.call(cached, &k)
28
- } || app.call(e){ |res|
29
- wrapped.call(res){ |res_wrapped|
30
- k.call(if (res_wrapped[FAIL] || []).empty?
31
- cache_for(res).merge(res_wrapped)
32
- else
33
- res_wrapped
34
- end)}}
25
+ k.call(cached)
26
+ } || app_call(e, &k)
27
+ end
28
+
29
+ def app_call env
30
+ app.call(env) do |res|
31
+ yield(if (res[FAIL] || []).empty?
32
+ cache_for(res)
33
+ else
34
+ res
35
+ end)
36
+ end
35
37
  end
36
38
 
37
39
  def cache_key env
@@ -5,7 +5,7 @@ class RestCore::DefaultHeaders
5
5
  def self.members; [:headers]; end
6
6
  include RestCore::Middleware
7
7
  def call env, &k
8
- app.call(env.merge(REQUEST_HEADERS =>
9
- @headers.merge(headers(env)).merge(env[REQUEST_HEADERS])), &k)
8
+ h = merge_hash(@headers, headers(env), env[REQUEST_HEADERS])
9
+ app.call(env.merge(REQUEST_HEADERS => h), &k)
10
10
  end
11
11
  end
@@ -4,32 +4,8 @@ require 'rest-core/middleware'
4
4
  class RestCore::DefaultPayload
5
5
  def self.members; [:payload]; end
6
6
  include RestCore::Middleware
7
-
8
- def initialize *args
9
- super
10
- @payload ||= {}
11
- end
12
-
13
7
  def call env, &k
14
- defaults = merge(@payload, payload(env))
15
-
16
- app.call(env.merge(REQUEST_PAYLOAD =>
17
- merge(defaults, env[REQUEST_PAYLOAD])), &k)
18
- end
19
-
20
- # this method is intended to merge payloads if they are non-empty hashes,
21
- # but prefer the right most one if they are not hashes.
22
- def merge lhs, rhs
23
- if rhs.respond_to?(:empty?) && rhs.empty?
24
- lhs
25
- elsif lhs.respond_to?(:merge)
26
- if rhs.respond_to?(:merge)
27
- string_keys(lhs).merge(string_keys(rhs))
28
- else
29
- rhs
30
- end
31
- else
32
- rhs
33
- end
8
+ p = merge_hash(@payload, payload(env), env[REQUEST_PAYLOAD])
9
+ app.call(env.merge(REQUEST_PAYLOAD => p), &k)
34
10
  end
35
11
  end
@@ -4,16 +4,8 @@ require 'rest-core/middleware'
4
4
  class RestCore::DefaultQuery
5
5
  def self.members; [:query]; end
6
6
  include RestCore::Middleware
7
-
8
- def initialize *args
9
- super
10
- @query ||= {}
11
- end
12
-
13
7
  def call env, &k
14
- defaults = string_keys(@query).merge(string_keys(query(env)))
15
-
16
- app.call(env.merge(REQUEST_QUERY =>
17
- defaults.merge(env[REQUEST_QUERY])), &k)
8
+ q = merge_hash(@query, query(env), env[REQUEST_QUERY])
9
+ app.call(env.merge(REQUEST_QUERY => q), &k)
18
10
  end
19
11
  end
@@ -10,7 +10,8 @@ class RestCore::JsonRequest
10
10
 
11
11
  def call env, &k
12
12
  return app.call(env, &k) unless json_request(env)
13
- return app.call(env, &k) unless env[REQUEST_PAYLOAD]
13
+ return app.call(env, &k) unless env[REQUEST_PAYLOAD] &&
14
+ !env[REQUEST_PAYLOAD].empty?
14
15
 
15
16
  app.call(env.merge(
16
17
  REQUEST_HEADERS => JSON_REQUEST_HEADER.merge(env[REQUEST_HEADERS]||{}),
@@ -9,7 +9,8 @@ class RestCore::JsonResponse
9
9
  class ParseError < Json.const_get(:ParseError)
10
10
  attr_reader :cause, :body
11
11
  def initialize cause, body
12
- super("#{cause.message}\nOriginal text: #{body}")
12
+ msg = cause.message.force_encoding('utf-8')
13
+ super("#{msg}\nOriginal text: #{body}")
13
14
  @cause, @body = cause, body
14
15
  end
15
16
  end
@@ -27,7 +28,9 @@ class RestCore::JsonResponse
27
28
  end
28
29
 
29
30
  def process response
30
- body = response[RESPONSE_BODY]
31
+ # StackExchange returns the problematic BOM! in UTF-8, so we need to
32
+ # strip it or it would break JSON parsers (i.e. yajl-ruby and json)
33
+ body = response[RESPONSE_BODY].to_s.sub(/\A\xEF\xBB\xBF/, '')
31
34
  response.merge(RESPONSE_BODY => Json.decode("[#{body}]").first)
32
35
  # [this].first is not needed for yajl-ruby
33
36
  rescue Json.const_get(:ParseError) => error
@@ -1,26 +1,17 @@
1
1
 
2
2
  require 'rest-core'
3
3
 
4
- require 'webmock'
4
+ require 'pork/auto'
5
5
  require 'muack'
6
- require 'bacon'
6
+ require 'webmock'
7
7
 
8
8
  # for testing lighten (serialization)
9
9
  require 'yaml'
10
10
 
11
11
  WebMock.disable_net_connect!(:allow_localhost => true)
12
- Bacon.summary_on_exit
13
- Bacon::Context.send(:include, Muack::API, WebMock::API)
12
+ Pork::Executor.__send__(:include, Muack::API, WebMock::API)
14
13
 
15
14
  module Kernel
16
- def eq? rhs
17
- self == rhs
18
- end
19
-
20
- def lt? rhs
21
- self < rhs
22
- end
23
-
24
15
  def with_img
25
16
  f = Tempfile.new(['img', '.jpg'])
26
17
  n = File.basename(f.path)
@@ -1,4 +1,4 @@
1
1
 
2
2
  module RestCore
3
- VERSION = '3.2.0'
3
+ VERSION = '3.3.0'
4
4
  end
data/rest-core.gemspec CHANGED
@@ -1,14 +1,14 @@
1
1
  # -*- encoding: utf-8 -*-
2
- # stub: rest-core 3.2.0 ruby lib
2
+ # stub: rest-core 3.3.0 ruby lib
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "rest-core"
6
- s.version = "3.2.0"
6
+ s.version = "3.3.0"
7
7
 
8
8
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
9
9
  s.require_paths = ["lib"]
10
10
  s.authors = ["Lin Jen-Shin (godfat)"]
11
- s.date = "2014-06-27"
11
+ s.date = "2014-08-25"
12
12
  s.description = "Modular Ruby clients interface for REST APIs.\n\nThere has been an explosion in the number of REST APIs available today.\nTo address the need for a way to access these APIs easily and elegantly,\nwe have developed rest-core, which consists of composable middleware\nthat allows you to build a REST client for any REST API. Or in the case of\ncommon APIs such as Facebook, Github, and Twitter, you can simply use the\ndedicated clients provided by [rest-more][].\n\n[rest-more]: https://github.com/godfat/rest-more"
13
13
  s.email = ["godfat (XD) godfat.org"]
14
14
  s.files = [
@@ -76,7 +76,6 @@ Gem::Specification.new do |s|
76
76
  "lib/rest-core/util/payload.rb",
77
77
  "lib/rest-core/util/smash.rb",
78
78
  "lib/rest-core/version.rb",
79
- "lib/rest-core/wrapper.rb",
80
79
  "rest-core.gemspec",
81
80
  "task/README.md",
82
81
  "task/gemgem.rb",
@@ -89,6 +88,7 @@ Gem::Specification.new do |s|
89
88
  "test/test_client.rb",
90
89
  "test/test_client_oauth1.rb",
91
90
  "test/test_config.rb",
91
+ "test/test_default_headers.rb",
92
92
  "test/test_default_payload.rb",
93
93
  "test/test_default_query.rb",
94
94
  "test/test_error_detector.rb",
@@ -111,11 +111,10 @@ Gem::Specification.new do |s|
111
111
  "test/test_smash_response.rb",
112
112
  "test/test_thread_pool.rb",
113
113
  "test/test_timeout.rb",
114
- "test/test_universal.rb",
115
- "test/test_wrapper.rb"]
114
+ "test/test_universal.rb"]
116
115
  s.homepage = "https://github.com/godfat/rest-core"
117
116
  s.licenses = ["Apache License 2.0"]
118
- s.rubygems_version = "2.2.2"
117
+ s.rubygems_version = "2.4.1"
119
118
  s.summary = "Modular Ruby clients interface for REST APIs."
120
119
  s.test_files = [
121
120
  "test/test_auth_basic.rb",
@@ -126,6 +125,7 @@ Gem::Specification.new do |s|
126
125
  "test/test_client.rb",
127
126
  "test/test_client_oauth1.rb",
128
127
  "test/test_config.rb",
128
+ "test/test_default_headers.rb",
129
129
  "test/test_default_payload.rb",
130
130
  "test/test_default_query.rb",
131
131
  "test/test_error_detector.rb",
@@ -148,8 +148,7 @@ Gem::Specification.new do |s|
148
148
  "test/test_smash_response.rb",
149
149
  "test/test_thread_pool.rb",
150
150
  "test/test_timeout.rb",
151
- "test/test_universal.rb",
152
- "test/test_wrapper.rb"]
151
+ "test/test_universal.rb"]
153
152
 
154
153
  if s.respond_to? :specification_version then
155
154
  s.specification_version = 4
@@ -157,15 +156,15 @@ Gem::Specification.new do |s|
157
156
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
158
157
  s.add_runtime_dependency(%q<httpclient>, [">= 0"])
159
158
  s.add_runtime_dependency(%q<mime-types>, [">= 0"])
160
- s.add_runtime_dependency(%q<timers>, [">= 0"])
159
+ s.add_runtime_dependency(%q<timers>, ["< 4"])
161
160
  else
162
161
  s.add_dependency(%q<httpclient>, [">= 0"])
163
162
  s.add_dependency(%q<mime-types>, [">= 0"])
164
- s.add_dependency(%q<timers>, [">= 0"])
163
+ s.add_dependency(%q<timers>, ["< 4"])
165
164
  end
166
165
  else
167
166
  s.add_dependency(%q<httpclient>, [">= 0"])
168
167
  s.add_dependency(%q<mime-types>, [">= 0"])
169
- s.add_dependency(%q<timers>, [">= 0"])
168
+ s.add_dependency(%q<timers>, ["< 4"])
170
169
  end
171
170
  end