elastomer-client 0.3.1

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 (49) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/.ruby-version +1 -0
  4. data/CHANGELOG.md +4 -0
  5. data/Gemfile +5 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +108 -0
  8. data/Rakefile +9 -0
  9. data/docs/notifications.md +71 -0
  10. data/elastomer-client.gemspec +30 -0
  11. data/lib/elastomer/client.rb +307 -0
  12. data/lib/elastomer/client/bulk.rb +257 -0
  13. data/lib/elastomer/client/cluster.rb +208 -0
  14. data/lib/elastomer/client/docs.rb +432 -0
  15. data/lib/elastomer/client/errors.rb +51 -0
  16. data/lib/elastomer/client/index.rb +407 -0
  17. data/lib/elastomer/client/multi_search.rb +115 -0
  18. data/lib/elastomer/client/nodes.rb +87 -0
  19. data/lib/elastomer/client/scan.rb +161 -0
  20. data/lib/elastomer/client/template.rb +85 -0
  21. data/lib/elastomer/client/warmer.rb +96 -0
  22. data/lib/elastomer/core_ext/time.rb +7 -0
  23. data/lib/elastomer/middleware/encode_json.rb +51 -0
  24. data/lib/elastomer/middleware/opaque_id.rb +69 -0
  25. data/lib/elastomer/middleware/parse_json.rb +39 -0
  26. data/lib/elastomer/notifications.rb +83 -0
  27. data/lib/elastomer/version.rb +7 -0
  28. data/script/bootstrap +16 -0
  29. data/script/cibuild +28 -0
  30. data/script/console +9 -0
  31. data/script/testsuite +10 -0
  32. data/test/assertions.rb +74 -0
  33. data/test/client/bulk_test.rb +226 -0
  34. data/test/client/cluster_test.rb +113 -0
  35. data/test/client/docs_test.rb +394 -0
  36. data/test/client/index_test.rb +244 -0
  37. data/test/client/multi_search_test.rb +129 -0
  38. data/test/client/nodes_test.rb +35 -0
  39. data/test/client/scan_test.rb +84 -0
  40. data/test/client/stubbed_client_tests.rb +40 -0
  41. data/test/client/template_test.rb +33 -0
  42. data/test/client/warmer_test.rb +56 -0
  43. data/test/client_test.rb +86 -0
  44. data/test/core_ext/time_test.rb +46 -0
  45. data/test/middleware/encode_json_test.rb +53 -0
  46. data/test/middleware/opaque_id_test.rb +39 -0
  47. data/test/middleware/parse_json_test.rb +54 -0
  48. data/test/test_helper.rb +94 -0
  49. metadata +210 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6cd17dd15348e159466cead916479b5bcf68b80b
4
+ data.tar.gz: b2f0b0e2455ad13ed6a3a1670cf20a58e6239501
5
+ SHA512:
6
+ metadata.gz: c9c2a2798145fa90c3291459de33dcdf01c1ad806b64e19b719fb6f5679c3b4b165583cb3ee4101a1f3e17879d15da93c350e88703256c1fab9755df80eda187
7
+ data.tar.gz: ce53c9838c420f6932d7792a088bdcd6ea7d3735e12f3df51d52459cc849b934cc91ff0703317574ad01ab75d0ca91ad0ef839d55cf7988a7417b499e52fcaed
@@ -0,0 +1,8 @@
1
+ /bin
2
+ /vendor/gems
3
+ /.bundle
4
+ /.rbenv-version
5
+ /vendor/cache/*.gem
6
+ /coverage
7
+ Gemfile.lock
8
+ *.gem
@@ -0,0 +1 @@
1
+ 2.1.0-github
@@ -0,0 +1,4 @@
1
+ ## 0.3.1 (2014-06-24)
2
+ - First rubygems release
3
+ - Make `update_aliases` more flexible
4
+ - Add `Client#semantic_version` for ES version comparisons
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ gem 'simplecov', :require => false, :group => :development, :platform => :ruby_19
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 GitHub Inc.
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,108 @@
1
+ # Elastomer
2
+
3
+ Making a stupid simple ElasticSearch client so your project can be smarter!
4
+
5
+ ## Getting Started
6
+
7
+ Use [Boxen](https://setup.githubapp.com) to get your machine set up. Then:
8
+
9
+ ```
10
+ $ git clone https://github.com/github/elastomer-client.git
11
+ $ cd elastomer-client
12
+ $ script/bootstrap
13
+ $ script/testsuite
14
+ ```
15
+
16
+ ## Client
17
+
18
+ The client provides a one-to-one mapping to the ElasticSearch [API
19
+ endpoints](http://www.elasticsearch.org/guide/reference/api/). The API is
20
+ decomposed into logical sections and accessed according to what you are trying
21
+ to accomplish. Each logical section is represented as a [client
22
+ class](lib/elastomer/client) and a top-level accessor is provided for each.
23
+
24
+ #### Cluster
25
+
26
+ API endpoints dealing with cluster level information and settings are found in
27
+ the [Cluster](lib/elastomer/client/cluster.rb) class.
28
+
29
+ ```ruby
30
+ require 'elastomer/client'
31
+ client = Elastomer::Client.new
32
+
33
+ # the current health summary
34
+ client.cluster.health
35
+
36
+ # detailed cluster state information
37
+ client.cluster.state
38
+
39
+ # the list of all index templates
40
+ client.cluster.templates
41
+ ```
42
+
43
+ #### Index
44
+
45
+ The methods in the [Index](lib/elastomer/client/index.rb) class deal with the
46
+ management of indexes in the cluster. This includes setting up type mappings
47
+ and adjusting settings. The actual indexing and search of documents are
48
+ handled by the Docs class (discussed next).
49
+
50
+ ```ruby
51
+ require 'elastomer/client'
52
+ client = Elastomer::Client.new
53
+
54
+ index = client.index('twitter')
55
+ index.create(
56
+ :settings => { 'index.number_of_shards' => 3 },
57
+ :mappings => {
58
+ :tweet => {
59
+ :_source => { :enabled => true },
60
+ :_all => { :enabled => false },
61
+ :properties => {
62
+ :author => { :type => 'string', :index => 'not_analyzed' },
63
+ :tweet => { :type => 'string', :analyze => 'standard' }
64
+ }
65
+ }
66
+ }
67
+ )
68
+
69
+ index.exists?
70
+
71
+ index.exists? :type => 'tweet'
72
+
73
+ index.delete
74
+ ```
75
+
76
+ #### Docs
77
+
78
+ This decomposition is the most questionable, but it's a starting point. The
79
+ [Docs](lib/elastomer/client/docs.rb) class handles the indexing and searching
80
+ of documents. Each instance is scoped to an index and optionally a document
81
+ type.
82
+
83
+ ```ruby
84
+ require 'elastomer/client'
85
+ client = Elastomer::Client.new
86
+
87
+ docs = client.docs('twitter')
88
+
89
+ docs.index({
90
+ :_id => 1,
91
+ :_type => 'tweet',
92
+ :author => '@pea53',
93
+ :tweet => 'announcing Elastomer, the stupid simple ElasticSearch client'
94
+ })
95
+
96
+ docs.search({:query => {:match_all => {}}}, :search_type => 'count')
97
+ ```
98
+
99
+ #### Performance
100
+
101
+ By default Elastomer uses Net::HTTP (via Faraday) to communicate with
102
+ ElasticSearch. You may find that Excon performs better for your use. To enable
103
+ Excon, add it to your bundle and then change your Elastomer initialization
104
+ thusly:
105
+
106
+ ```
107
+ Elastomer::Client.new(url: YOUR_ES_URL, adapter: :excon)
108
+ ```
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.pattern = "test/**/*_test.rb"
7
+ end
8
+
9
+ task :default => :test
@@ -0,0 +1,71 @@
1
+ # Notifications Support
2
+
3
+ Requiring `elastomer/notifications` enables support for broadcasting
4
+ elastomer events through ActiveSupport::Notifications.
5
+
6
+ The event namespace is `request.client.elastomer`.
7
+
8
+ ## Sample event payload
9
+
10
+ ```
11
+ :index => "index-test",
12
+ :type => nil,
13
+ :action => "docs.search",
14
+ :context=> nil,
15
+ :body => "{\"query\":{\"match_all\":{}}}",
16
+ :url => #<URI::HTTP:0x007fb6f3e98b60 URL:http://localhost:19200/index-test/_search?search_type=count>,
17
+ :method => :get,
18
+ :status => 200}
19
+ ```
20
+
21
+ ## Valid actions
22
+ - bulk
23
+ - cluster.get_settings
24
+ - cluster.health
25
+ - cluster.reroute
26
+ - cluster.state
27
+ - cluster.update_settings
28
+ - cluster.available
29
+ - cluster.get_aliases
30
+ - cluster.info
31
+ - cluster.shutdown
32
+ - cluster.update_aliases
33
+ - docs.delete
34
+ - docs.delete_by_query
35
+ - docs.explain
36
+ - docs.get
37
+ - docs.index
38
+ - docs.more_like_this
39
+ - docs.multi_get
40
+ - docs.search
41
+ - docs.source
42
+ - docs.update
43
+ - docs.validate
44
+ - index.analyze
45
+ - index.clear_cache
46
+ - index.close
47
+ - index.create
48
+ - index.delete
49
+ - index.delete_mapping
50
+ - index.exists
51
+ - index.flush
52
+ - index.get_aliases
53
+ - index.get_settings
54
+ - index.mapping
55
+ - index.open
56
+ - index.optimize
57
+ - index.refresh
58
+ - index.segments
59
+ - index.snapshot
60
+ - index.stats
61
+ - index.status
62
+ - index.update_mapping
63
+ - index.update_settings
64
+ - nodes.hot_threads
65
+ - nodes.info
66
+ - nodes.shutdown
67
+ - nodes.stats
68
+ - search.scan
69
+ - search.scroll
70
+ - template.create
71
+ - template.get
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'elastomer/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "elastomer-client"
8
+ spec.version = Elastomer::VERSION
9
+ spec.authors = ["Tim Pease", "Grant Rodgers"]
10
+ spec.email = ["tim.pease@github.com", "grant.rodgers@github.com"]
11
+ spec.summary = %q{A library for interacting with the GitHub Search infrastructure}
12
+ spec.description = %q{Elastomer is a low level API client for the
13
+ Elasticsearch HTTP interface.}
14
+ spec.homepage = "https://github.com/github/elastomer-client"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0")
18
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
19
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_dependency "addressable", "~> 2.3"
23
+ spec.add_dependency "faraday", "~> 0.8"
24
+ spec.add_dependency "multi_json", "~> 1.7"
25
+ spec.add_dependency "semantic", "~> 1.3"
26
+
27
+ spec.add_development_dependency "bundler", "~> 1.5"
28
+ spec.add_development_dependency "minitest","~> 4.7"
29
+ spec.add_development_dependency "rake"
30
+ end
@@ -0,0 +1,307 @@
1
+ require 'addressable/template'
2
+ require 'faraday'
3
+ require 'multi_json'
4
+ require 'semantic'
5
+
6
+ require 'elastomer/version'
7
+
8
+ module Elastomer
9
+
10
+ class Client
11
+
12
+ # Create a new client that can be used to make HTTP requests to the
13
+ # ElasticSearch server.
14
+ #
15
+ # opts - The options Hash
16
+ # :host - the host as a String
17
+ # :port - the port number of the server
18
+ # :url - the URL as a String (overrides :host and :port)
19
+ # :read_timeout - the timeout in seconds when reading from an HTTP connection
20
+ # :open_timeout - the timeout in seconds when opening an HTTP connection
21
+ # :adapter - the Faraday adapter to use (defaults to :excon)
22
+ # :opaque_id - set to `true` to use the 'X-Opaque-Id' request header
23
+ #
24
+ def initialize( opts = {} )
25
+ host = opts.fetch :host, 'localhost'
26
+ port = opts.fetch :port, 9200
27
+ @url = opts.fetch :url, "http://#{host}:#{port}"
28
+
29
+ uri = Addressable::URI.parse @url
30
+ @host = uri.host
31
+ @port = uri.port
32
+
33
+ @read_timeout = opts.fetch :read_timeout, 5
34
+ @open_timeout = opts.fetch :open_timeout, 2
35
+ @adapter = opts.fetch :adapter, Faraday.default_adapter
36
+ @opaque_id = opts.fetch :opaque_id, false
37
+ end
38
+
39
+ attr_reader :host, :port, :url
40
+ attr_reader :read_timeout, :open_timeout
41
+
42
+ # Returns true if the server is available; returns false otherwise.
43
+ def available?
44
+ response = head '/', :action => 'cluster.available'
45
+ response.success?
46
+ rescue StandardError
47
+ false
48
+ end
49
+
50
+ # Returns the version String of the attached ElasticSearch instance.
51
+ def version
52
+ @version ||= info['version']['number']
53
+ end
54
+
55
+ # Returns a Semantic::Version for the attached ElasticSearch instance.
56
+ # See https://rubygems.org/gems/semantic
57
+ def semantic_version
58
+ Semantic::Version.new(version)
59
+ end
60
+
61
+ # Returns the information Hash from the attached ElasticSearch instance.
62
+ def info
63
+ response = get '/', :action => 'cluster.info'
64
+ response.body
65
+ end
66
+
67
+ # Internal: Provides access to the Faraday::Connection used by this client
68
+ # for all requests to the server.
69
+ #
70
+ # Returns a Faraday::Connection
71
+ def connection
72
+ @connection ||= Faraday.new(url) do |conn|
73
+ conn.request :encode_json
74
+ conn.response :parse_json
75
+ conn.request :opaque_id if @opaque_id
76
+
77
+ Array === @adapter ?
78
+ conn.adapter(*@adapter) :
79
+ conn.adapter(@adapter)
80
+
81
+ conn.options[:timeout] = read_timeout
82
+ conn.options[:open_timeout] = open_timeout
83
+ end
84
+ end
85
+
86
+ # Internal: Sends an HTTP HEAD request to the server.
87
+ #
88
+ # path - The path as a String
89
+ # params - Parameters Hash
90
+ #
91
+ # Returns a Faraday::Response
92
+ def head( path, params = {} )
93
+ request :head, path, params
94
+ end
95
+
96
+ # Internal: Sends an HTTP GET request to the server.
97
+ #
98
+ # path - The path as a String
99
+ # params - Parameters Hash
100
+ #
101
+ # Returns a Faraday::Response
102
+ # Raises an Elastomer::Client::Error on 4XX and 5XX responses
103
+ def get( path, params = {} )
104
+ request :get, path, params
105
+ end
106
+
107
+ # Internal: Sends an HTTP PUT request to the server.
108
+ #
109
+ # path - The path as a String
110
+ # params - Parameters Hash
111
+ #
112
+ # Returns a Faraday::Response
113
+ # Raises an Elastomer::Client::Error on 4XX and 5XX responses
114
+ def put( path, params = {} )
115
+ request :put, path, params
116
+ end
117
+
118
+ # Internal: Sends an HTTP POST request to the server.
119
+ #
120
+ # path - The path as a String
121
+ # params - Parameters Hash
122
+ #
123
+ # Returns a Faraday::Response
124
+ # Raises an Elastomer::Client::Error on 4XX and 5XX responses
125
+ def post( path, params = {} )
126
+ request :post, path, params
127
+ end
128
+
129
+ # Internal: Sends an HTTP DELETE request to the server.
130
+ #
131
+ # path - The path as a String
132
+ # params - Parameters Hash
133
+ #
134
+ # Returns a Faraday::Response
135
+ # Raises an Elastomer::Client::Error on 4XX and 5XX responses
136
+ def delete( path, params = {} )
137
+ request :delete, path, params
138
+ end
139
+
140
+ # Internal: Sends an HTTP request to the server. If the `params` Hash
141
+ # contains a :body key, it will be deleted from the Hash and the value
142
+ # will be used as the body of the request.
143
+ #
144
+ # method - The HTTP method to send [:head, :get, :put, :post, :delete]
145
+ # path - The path as a String
146
+ # params - Parameters Hash
147
+ # :body - Will be used as the request body
148
+ # :read_timeout - Optional read timeout (in seconds) for the request
149
+ #
150
+ # Returns a Faraday::Response
151
+ # Raises an Elastomer::Client::Error on 4XX and 5XX responses
152
+ def request( method, path, params )
153
+ body = params.delete :body
154
+ body = MultiJson.dump body if Hash === body
155
+
156
+ read_timeout = params.delete :read_timeout
157
+
158
+ path = expand_path path, params
159
+
160
+ response = instrument(path, body, params) do
161
+ case method
162
+ when :head
163
+ connection.head(path) { |req| req.options[:timeout] = read_timeout if read_timeout }
164
+
165
+ when :get
166
+ connection.get(path) { |req|
167
+ req.body = body if body
168
+ req.options[:timeout] = read_timeout if read_timeout
169
+ }
170
+
171
+ when :put
172
+ connection.put(path, body) { |req| req.options[:timeout] = read_timeout if read_timeout }
173
+
174
+ when :post
175
+ connection.post(path, body) { |req| req.options[:timeout] = read_timeout if read_timeout }
176
+
177
+ when :delete
178
+ connection.delete(path) { |req|
179
+ req.body = body if body
180
+ req.options[:timeout] = read_timeout if read_timeout
181
+ }
182
+
183
+ else
184
+ raise ArgumentError, "unknown HTTP request method: #{method.inspect}"
185
+ end
186
+ end
187
+
188
+ handle_errors response
189
+
190
+ rescue Faraday::Error::TimeoutError => boom
191
+ raise ::Elastomer::Client::TimeoutError.new(boom, path)
192
+
193
+ # ensure
194
+ # # FIXME: this is here until we get a real logger in place
195
+ # STDERR.puts "[#{response.status.inspect}] curl -X#{method.to_s.upcase} '#{url}#{path}'" unless response.nil?
196
+ end
197
+
198
+ # Internal: Apply path expansions to the `path` and append query
199
+ # parameters to the `path`. We are using an Addressable::Template to
200
+ # replace '{expansion}' fields found in the path with the values extracted
201
+ # from the `params` Hash. Any remaining elements in the `params` hash are
202
+ # treated as query parameters and appended to the end of the path.
203
+ #
204
+ # path - The path as a String
205
+ # params - Parameters Hash
206
+ #
207
+ # Examples
208
+ #
209
+ # expand_path('/foo{/bar}', {:bar => 'hello', :q => 'what', :p => 2})
210
+ # #=> '/foo/hello?q=what&p=2'
211
+ #
212
+ # expand_path('/foo{/bar}{/baz}', {:baz => 'no bar'}
213
+ # #=> '/foo/no%20bar'
214
+ #
215
+ # Returns an Addressable::Uri
216
+ def expand_path( path, params )
217
+ template = Addressable::Template.new path
218
+
219
+ expansions = {}
220
+ query_values = params.dup
221
+ query_values.delete :action
222
+ query_values.delete :context
223
+
224
+ template.keys.map(&:to_sym).each do |key|
225
+ value = query_values.delete key
226
+ value = assert_param_presence(value, key) unless path =~ /{\/#{key}}/ && value.nil?
227
+ expansions[key] = value
228
+ end
229
+
230
+ uri = template.expand(expansions)
231
+ uri.query_values = query_values unless query_values.empty?
232
+ uri.to_s
233
+ end
234
+
235
+ # Internal: A noop method that simply yields to the block. This method
236
+ # will be replaced when the 'elastomer/notifications' module is included.
237
+ #
238
+ # path - The full request path as a String
239
+ # body - The request body as a String or `nil`
240
+ # params - The request params Hash
241
+ # block - The block that will be instrumented
242
+ #
243
+ # Returns the response from the block
244
+ def instrument( path, body, params )
245
+ yield
246
+ end
247
+
248
+ # Internal: Inspect the Faraday::Response and raise an error if the status
249
+ # is in the 5XX range or if the response body contains an 'error' field.
250
+ # In the latter case, the value of the 'error' field becomes our exception
251
+ # message. In the absence of an 'error' field the response body is used
252
+ # as the exception message.
253
+ #
254
+ # The raised exception will contain the response object.
255
+ #
256
+ # response - The Faraday::Response object.
257
+ #
258
+ # Returns the response.
259
+ # Raises an Elastomer::Client::Error on 500 responses or responses
260
+ # containing and 'error' field.
261
+ def handle_errors( response )
262
+ raise Error, response if response.status >= 500
263
+ raise Error, response if Hash === response.body && response.body['error']
264
+
265
+ response
266
+ end
267
+
268
+ # Internal: Ensure that the parameter has a valid value. Things like `nil`
269
+ # and empty strings are right out. This method also performs a little
270
+ # formating on the parameter:
271
+ #
272
+ # * leading and trailing whitespace is removed
273
+ # * arrays are flattend
274
+ # * and then joined into a String
275
+ # * numerics are converted to their string equivalents
276
+ #
277
+ # param - The param Object to validate
278
+ # name - Optional param name as a String (used in exception messages)
279
+ #
280
+ # Returns the validated param as a String.
281
+ # Raises an ArgumentError if the param is not valid.
282
+ def assert_param_presence( param, name = 'input value' )
283
+ case param
284
+ when String, Numeric
285
+ param = param.to_s.strip
286
+ raise ArgumentError, "#{name} cannot be blank: #{param.inspect}" if param =~ /\A\s*\z/
287
+ param
288
+
289
+ when Array
290
+ param.flatten.map { |item| assert_param_presence(item, name) }.join(',')
291
+
292
+ when nil
293
+ raise ArgumentError, "#{name} cannot be nil"
294
+
295
+ else
296
+ raise ArgumentError, "#{name} is invalid: #{param.inspect}"
297
+ end
298
+ end
299
+
300
+ end # Client
301
+ end # Elastomer
302
+
303
+ # require all files in the `client` sub-directory
304
+ Dir.glob(File.expand_path('../client/*.rb', __FILE__)).each { |fn| require fn }
305
+
306
+ # require all files in the `middleware` sub-directory
307
+ Dir.glob(File.expand_path('../middleware/*.rb', __FILE__)).each { |fn| require fn }