"], "crystal" => "0.25.0", "license" => "MIT"}
-```
-
-#### Binary Data
-
-Store binary data (eg, `application/octet-stream`) to file, you can use [streaming requests](#streaming-requests):
-
-```crystal
-Halite.get("https://github.com/icyleaf/halite/archive/master.zip") do |response|
- filename = response.filename || "halite-master.zip"
- File.open(filename, "w") do |file|
- IO.copy(response.body_io, file)
- end
-end
-```
-
-### Error Handling
-
-- For any status code, a `Halite::Response` will be returned.
-- If request timeout, a `Halite::TimeoutError` will be raised.
-- If a request exceeds the configured number of maximum redirections, a `Halite::TooManyRedirectsError` will raised.
-- If request uri is http and configured tls context, a `Halite::RequestError` will raised.
-- If request uri is invalid, a `Halite::ConnectionError`/`Halite::UnsupportedMethodError`/`Halite::UnsupportedSchemeError` will raised.
-
-#### Raise for status code
-
-If we made a bad request(a 4xx client error or a 5xx server error response), we can raise with `Halite::Response.raise_for_status`.
-
-But, since our `status_code` was not `4xx` or `5xx`, it returns `nil` when we call it:
-
-```crystal
-urls = [
- "https://httpbin.org/status/404",
- "https://httpbin.org/status/500?foo=bar",
- "https://httpbin.org/status/200",
-]
-
-urls.each do |url|
- r = Halite.get url
- begin
- r.raise_for_status
- p r.body
- rescue ex : Halite::ClientError | Halite::ServerError
- p "[#{ex.status_code}] #{ex.status_message} (#{ex.class})"
- end
-end
-
-# => "[404] not found error with url: https://httpbin.org/status/404 (Halite::Exception::ClientError)"
-# => "[500] internal server error error with url: https://httpbin.org/status/500?foo=bar (Halite::Exception::ServerError)"
-# => ""
-```
-
-## Middleware
-
-Halite now has middleware (a.k.a features) support providing a simple way to plug in intermediate custom logic
-in your HTTP client, allowing you to monitor outgoing requests, incoming responses, and use it as an interceptor.
-
-Available features:
-
-- [Logging](#logging) (Yes, logging is based on feature, cool, aha!)
-- [Local Cache](#local-cache) (local storage, speed up in development)
-
-### Write a simple feature
-
-Let's implement simple middleware that prints each request:
-
-```crystal
-class RequestMonister < Halite::Feature
- @label : String
- def initialize(**options)
- @label = options.fetch(:label, "")
- end
-
- def request(request) : Halite::Request
- puts @label
- puts request.verb
- puts request.uri
- puts request.body
-
- request
- end
-
- Halite.register_feature "request_monster", self
-end
-```
-
-Then use it in Halite:
-
-```crystal
-Halite.use("request_monster", label: "testing")
- .post("http://httpbin.org/post", form: {name: "foo"})
-
-# Or configure to client
-client = Halite::Client.new do
- use "request_monster", label: "testing"
-end
-
-client.post("http://httpbin.org/post", form: {name: "foo"})
-
-# => testing
-# => POST
-# => http://httpbin.org/post
-# => name=foo
-```
-
-### Write a interceptor
-
-Halite's killer feature is the **interceptor**, Use `Halite::Feature::Chain` to process with two result:
-
-- `next`: perform and run next interceptor
-- `return`: perform and return
-
-So, you can intercept and turn to the following registered features.
-
-```crystal
-class AlwaysNotFound < Halite::Feature
- def intercept(chain)
- response = chain.perform
- response = Halite::Response.new(chain.request.uri, 404, response.body, response.headers)
- chain.next(response)
- end
-
- Halite.register_feature "404", self
-end
-
-class PoweredBy < Halite::Feature
- def intercept(chain)
- if response = chain.response
- response.headers["X-Powered-By"] = "Halite"
- chain.return(response)
- else
- chain
- end
- end
-
- Halite.register_feature "powered_by", self
-end
-
-r = Halite.use("404").use("powered_by").get("http://httpbin.org/user-agent")
-r.status_code # => 404
-r.headers["X-Powered-By"] # => Halite
-r.body # => {"user-agent":"Halite/0.6.0"}
-```
-
-For more implementation details about the feature layer, see the [Feature](https://github.com/icyleaf/halite/blob/master/src/halite/feature.cr#L2) class and [examples](https://github.com/icyleaf/halite/tree/master/src/halite/features) and [specs](https://github.com/icyleaf/halite/blob/master/spec/spec_helper.cr#L23).
-
-## Advanced Usage
-
-### Configuring
-
-Halite provides a traditional way to instance client, and you can configure any chainable methods with block:
-
-```crystal
-client = Halite::Client.new do
- # Set basic auth
- basic_auth "username", "password"
-
- # Enable logging
- logging true
-
- # Set timeout
- timeout 10.seconds
-
- # Set user agent
- headers user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36"
-end
-
-# You also can configure in this way
-client.accept("application/json")
-
-r = client.get("http://httpbin.org/get")
-```
-
-### Endpoint
-
-No more given endpoint per request, use `endpoint` will make the request URI shorter, you can set it in flexible way:
-
-```crystal
-client = Halite::Client.new do
- endpoint "https://gitlab.org/api/v4"
- user_agent "Halite"
-end
-
-client.get("users") # GET https://gitlab.org/api/v4/users
-
-# You can override the path by using an absolute path
-client.get("/users") # GET https://gitlab.org/users
-```
-
-### Sessions
-
-As like [requests.Session()](http://docs.python-requests.org/en/master/user/advanced/#session-objects), Halite built-in session by default.
-
-Let's persist some cookies across requests:
-
-```crystal
-client = Halite::Client.new
-client.get("http://httpbin.org/cookies/set?private_token=6abaef100b77808ceb7fe26a3bcff1d0")
-client.get("http://httpbin.org/cookies")
-# => 2018-06-25 18:41:05 +08:00 | request | GET | http://httpbin.org/cookies/set?private_token=6abaef100b77808ceb7fe26a3bcff1d0
-# => 2018-06-25 18:41:06 +08:00 | response | 302 | http://httpbin.org/cookies/set?private_token=6abaef100b77808ceb7fe26a3bcff1d0 | text/html
-# =>
-# => Redirecting...
-# => Redirecting...
-# => You should be redirected automatically to target URL: /cookies. If not click the link.
-# => 2018-06-25 18:41:06 +08:00 | request | GET | http://httpbin.org/cookies
-# => 2018-06-25 18:41:07 +08:00 | response | 200 | http://httpbin.org/cookies | application/json
-# => {"cookies":{"private_token":"6abaef100b77808ceb7fe26a3bcff1d0"}}
-```
-
-All it support with [chainable methods](https://icyleaf.github.io/halite/Halite/Chainable.html) in the other examples list in [requests.Session](http://docs.python-requests.org/en/master/user/advanced/#session-objects).
-
-Note, however, that chainable methods will not be persisted across requests, even if using a session. This example will only send the cookies or headers with the first request, but not the second:
-
-```crystal
-client = Halite::Client.new
-r = client.cookies("username": "foobar").get("http://httpbin.org/cookies")
-r.body # => {"cookies":{"username":"foobar"}}
-
-r = client.get("http://httpbin.org/cookies")
-r.body # => {"cookies":{}}
-```
-
-If you want to manually add cookies, headers (even features etc) to your session, use the methods start with `with_` in `Halite::Options`
-to manipulate them:
-
-```crystal
-r = client.get("http://httpbin.org/cookies")
-r.body # => {"cookies":{}}
-
-client.options.with_cookie("username": "foobar")
-r = client.get("http://httpbin.org/cookies")
-r.body # => {"cookies":{"username":"foobar"}}
-```
-
-### Streaming Requests
-
-Similar to [HTTP::Client](https://crystal-lang.org/api/0.36.1/HTTP/Client.html#streaming) usage with a block,
-you can easily use same way, but Halite returns a `Halite::Response` object:
-
-```crystal
-r = Halite.get("http://httpbin.org/stream/5") do |response|
- response.status_code # => 200
- response.body_io.each_line do |line|
- puts JSON.parse(line) # => {"url" => "http://httpbin.org/stream/5", "args" => {}, "headers" => {"Host" => "httpbin.org", "Connection" => "close", "User-Agent" => "Halite/0.8.0", "Accept" => "*/*", "Accept-Encoding" => "gzip, deflate"}, "id" => 0_i64}
- end
-end
-```
-
-> **Warning**:
->
-> `body_io` is avaiabled as an `IO` and not reentrant safe. Might throws a "Nil assertion failed" exception if there is no data in the `IO`
-(such like `head` requests). Calling this method multiple times causes some of the received data being lost.
->
-> One more thing, use streaming requests the response will always [enable redirect](#redirects-and-history) automatically.
-
-### Logging
-
-Halite does not enable logging on each request and response too.
-We can enable per operation logging by configuring them through the chaining API.
-
-By default, Halite will logging all outgoing HTTP requests and their responses(without binary stream) to `STDOUT` on DEBUG level.
-You can configuring the following options:
-
-- `logging`: Instance your `Halite::Logging::Abstract`, check [Use the custom logging](#use-the-custom-logging).
-- `format`: Output format, built-in `common` and `json`, you can write your own.
-- `file`: Write to file with path, works with `format`.
-- `filemode`: Write file mode, works with `format`, by default is `a`. (append to bottom, create it if file is not exist)
-- `skip_request_body`: By default is `false`.
-- `skip_response_body`: By default is `false`.
-- `skip_benchmark`: Display elapsed time, by default is `false`.
-- `colorize`: Enable colorize in terminal, only apply in `common` format, by default is `true`.
-
-> **NOTE**: `format` (`file` and `filemode`) and `logging` are conflict, you can not use both.
-
-Let's try with it:
-
-```crystal
-# Logging json request
-Halite.logging
- .get("http://httpbin.org/get", params: {name: "foobar"})
-
-# => 2018-06-25 18:33:14 +08:00 | request | GET | http://httpbin.org/get?name=foobar
-# => 2018-06-25 18:33:15 +08:00 | response | 200 | http://httpbin.org/get?name=foobar | 381.32ms | application/json
-# => {"args":{"name":"foobar"},"headers":{"Accept":"*/*","Accept-Encoding":"gzip, deflate","Connection":"close","Host":"httpbin.org","User-Agent":"Halite/0.3.2"},"origin":"60.206.194.34","url":"http://httpbin.org/get?name=foobar"}
-
-# Logging image request
-Halite.logging
- .get("http://httpbin.org/image/png")
-
-# => 2018-06-25 18:34:15 +08:00 | request | GET | http://httpbin.org/image/png
-# => 2018-06-25 18:34:15 +08:00 | response | 200 | http://httpbin.org/image/png | image/png
-
-# Logging with options
-Halite.logging(skip_request_body: true, skip_response_body: true)
- .post("http://httpbin.org/get", form: {image: File.open("halite-logo.png")})
-
-# => 2018-08-28 14:33:19 +08:00 | request | POST | http://httpbin.org/post
-# => 2018-08-28 14:33:21 +08:00 | response | 200 | http://httpbin.org/post | 1.61s | application/json
-```
-
-#### JSON-formatted logging
-
-It has JSON formatted for developer friendly logging.
-
-```
-Halite.logging(format: "json")
- .get("http://httpbin.org/get", params: {name: "foobar"})
-```
-
-#### Write to a log file
-
-```crystal
-# Write plain text to a log file
-Log.setup("halite.file", backend: Log::IOBackend.new(File.open("/tmp/halite.log", "a")))
-Halite.logging(for: "halite.file", skip_benchmark: true, colorize: false)
- .get("http://httpbin.org/get", params: {name: "foobar"})
-
-# Write json data to a log file
-Log.setup("halite.file", backend: Log::IOBackend.new(File.open("/tmp/halite.log", "a")))
-Halite.logging(format: "json", for: "halite.file")
- .get("http://httpbin.org/get", params: {name: "foobar"})
-
-# Redirect *all* logging from Halite to a file:
-Log.setup("halite", backend: Log::IOBackend.new(File.open("/tmp/halite.log", "a")))
-```
-
-#### Use the custom logging
-
-Creating the custom logging by integration `Halite::Logging::Abstract` abstract class.
-Here has two methods must be implement: `#request` and `#response`.
-
-```crystal
-class CustomLogging < Halite::Logging::Abstract
- def request(request)
- @logger.info { "| >> | %s | %s %s" % [request.verb, request.uri, request.body] }
- end
-
- def response(response)
- @logger.info { "| << | %s | %s %s" % [response.status_code, response.uri, response.content_type] }
- end
-end
-
-# Add to adapter list (optional)
-Halite::Logging.register "custom", CustomLogging.new
-
-Halite.logging(logging: CustomLogging.new)
- .get("http://httpbin.org/get", params: {name: "foobar"})
-
-# We can also call it use format name if you added it.
-Halite.logging(format: "custom")
- .get("http://httpbin.org/get", params: {name: "foobar"})
-
-# => 2017-12-13 16:40:13 +08:00 | >> | GET | http://httpbin.org/get?name=foobar
-# => 2017-12-13 16:40:15 +08:00 | << | 200 | http://httpbin.org/get?name=foobar application/json
-```
-
-### Local Cache
-
-Local cache feature is caching responses easily with Halite through an chainable method that is simple and elegant
-yet powerful. Its aim is to focus on the HTTP part of caching and do not worrying about how stuff stored, api rate limiting
-even works without network(offline).
-
-It has the following options:
-
-- `file`: Load cache from file. it conflict with `path` and `expires`.
-- `path`: The path of cache, default is "/tmp/halite/cache/"
-- `expires`: The expires time of cache, default is never expires.
-- `debug`: The debug mode of cache, default is `true`
-
-With debug mode, cached response it always included some headers information:
-
-- `X-Halite-Cached-From`: Cache source (cache or file)
-- `X-Halite-Cached-Key`: Cache key with verb, uri and body (return with cache, not `file` passed)
-- `X-Halite-Cached-At`: Cache created time
-- `X-Halite-Cached-Expires-At`: Cache expired time (return with cache, not `file` passed)
-
-```crystal
-Halite.use("cache").get "http://httpbin.org/anything" # request a HTTP
-r = Halite.use("cache").get "http://httpbin.org/anything" # request from local storage
-r.headers # => {..., "X-Halite-Cached-At" => "2018-08-30 10:41:14 UTC", "X-Halite-Cached-By" => "Halite", "X-Halite-Cached-Expires-At" => "2018-08-30 10:41:19 UTC", "X-Halite-Cached-Key" => "2bb155e6c8c47627da3d91834eb4249a"}}
-```
-
-### Link Headers
-
-Many HTTP APIs feature [Link headers](https://tools.ietf.org/html/rfc5988). GitHub uses
-these for [pagination](https://developer.github.com/v3/#pagination) in their API, for example:
-
-```crystal
-r = Halite.get "https://api.github.com/users/icyleaf/repos?page=1&per_page=2"
-r.links
-# => {"next" =>
-# => Halite::HeaderLink(
-# => @params={},
-# => @rel="next",
-# => @target="https://api.github.com/user/17814/repos?page=2&per_page=2"),
-# => "last" =>
-# => Halite::HeaderLink(
-# => @params={},
-# => @rel="last",
-# => @target="https://api.github.com/user/17814/repos?page=41&per_page=2")}
-
-r.links["next"]
-# => "https://api.github.com/user/17814/repos?page=2&per_page=2"
-
-r.links["next"].params
-# => {}
-```
-
-## Help and Discussion
-
-You can browse the API documents:
-
-https://icyleaf.github.io/halite/
-
-You can browse the all chainable methods:
-
-https://icyleaf.github.io/halite/Halite/Chainable.html
-
-You can browse the Changelog:
-
-https://github.com/icyleaf/halite/blob/master/CHANGELOG.md
-
-If you have found a bug, please create a issue here:
-
-https://github.com/icyleaf/halite/issues/new
-
-## Donate
-
-Halite is a open source, collaboratively funded project. If you run a business and are using Halite in a revenue-generating product,
-it would make business sense to sponsor Halite development. Individual users are also welcome to make a one time donation
-if Halite has helped you in your work or personal projects.
-
-You can donate via [Paypal](https://www.paypal.me/icyleaf/5).
-
-## How to Contribute
-
-Your contributions are always welcome! Please submit a pull request or create an issue to add a new question, bug or feature to the list.
-
-All [Contributors](https://github.com/icyleaf/halite/graphs/contributors) are on the wall.
-
-## You may also like
-
-- [totem](https://github.com/icyleaf/totem) - Load and parse a configuration file or string in JSON, YAML, dotenv formats.
-- [markd](https://github.com/icyleaf/markd) - Yet another markdown parser built for speed, Compliant to CommonMark specification.
-- [poncho](https://github.com/icyleaf/poncho) - A .env parser/loader improved for performance.
-- [popcorn](https://github.com/icyleaf/popcorn) - Easy and Safe casting from one type to another.
-- [fast-crystal](https://github.com/icyleaf/fast-crystal) - ๐จ Writing Fast Crystal ๐ -- Collect Common Crystal idioms.
-
-## License
-
-[MIT License](https://github.com/icyleaf/halite/blob/master/LICENSE) ยฉ icyleaf
diff --git a/lib/halite/benchmarks/.gitignore b/lib/halite/benchmarks/.gitignore
deleted file mode 100644
index fa22f9c8..00000000
--- a/lib/halite/benchmarks/.gitignore
+++ /dev/null
@@ -1,17 +0,0 @@
-doc/
-docs/
-lib/
-bin/
-logs/
-.shards/
-
-# Local test file
-main.cr
-
-# Temporay ignore
-benchmarks
-
-# Libraries don't need dependency lock
-# Dependencies will be locked in application that uses them
-shard.lock
-.history/
diff --git a/lib/halite/benchmarks/README.md b/lib/halite/benchmarks/README.md
deleted file mode 100644
index b79406e3..00000000
--- a/lib/halite/benchmarks/README.md
+++ /dev/null
@@ -1,30 +0,0 @@
-# About this benchmarks
-
-Benchmarks performed inspired from excon's benchmarking tool.
-
-## Environments
-
-- MacBook Pro (Retina, 15-inch, Mid 2015), 2.2 GHz Intel Core i7, 16 GB 1600 MHz DDR3.
-- Crystal 0.35.1 (2020-06-19) LLVM: 10.0.0
-- Clients
- - buit-in HTTP::Client
- - create v0.26.1
- - halite v0.10.8
-
-## Result
-
-```
-Tach times: 10000
- Tach Total
- crest 8.0365ms
- halite 7.9538ms (fastest)
- halite (persistent) 8.0205ms
- built-in HTTP::Client 8.0256ms
-```
-
-## Test yourself
-
-```crystal
-$ shards build --release --no-debug
-$ ./bin/run_benchmark
-```
diff --git a/lib/halite/benchmarks/shard.yml b/lib/halite/benchmarks/shard.yml
deleted file mode 100644
index aac4f20d..00000000
--- a/lib/halite/benchmarks/shard.yml
+++ /dev/null
@@ -1,20 +0,0 @@
-name: benchmarks
-version: 0.1.0
-
-authors:
- - icyleaf
-
-targets:
- run_benchmark:
- main: src/run_benchmark.cr
-
-dependencies:
- crest:
- github: mamantoha/crest
- version: ~> 0.26.1
- halite:
- path: ../
-
-crystal: 0.35.1
-
-license: MIT
diff --git a/lib/halite/benchmarks/src/clients/crest.cr b/lib/halite/benchmarks/src/clients/crest.cr
deleted file mode 100644
index daeab1de..00000000
--- a/lib/halite/benchmarks/src/clients/crest.cr
+++ /dev/null
@@ -1,10 +0,0 @@
-require "crest"
-
-module Client
- MEMBERS << {
- name: "crest",
- proc: ->(url : String) {
- Crest.get(url).body
- },
- }
-end
diff --git a/lib/halite/benchmarks/src/clients/halite.cr b/lib/halite/benchmarks/src/clients/halite.cr
deleted file mode 100644
index 733989c3..00000000
--- a/lib/halite/benchmarks/src/clients/halite.cr
+++ /dev/null
@@ -1,17 +0,0 @@
-require "halite"
-
-module Client
- MEMBERS << {
- name: "halite",
- proc: ->(url : String) {
- Halite::Client.new.request("get", url).body
- },
- }
-
- MEMBERS << {
- name: "halite (persistent)",
- proc: ->(url : String) {
- Halite.get(url).body
- },
- }
-end
diff --git a/lib/halite/benchmarks/src/clients/http_client.cr b/lib/halite/benchmarks/src/clients/http_client.cr
deleted file mode 100644
index aab267db..00000000
--- a/lib/halite/benchmarks/src/clients/http_client.cr
+++ /dev/null
@@ -1,10 +0,0 @@
-require "http/server"
-
-module Client
- MEMBERS << {
- name: "built-in HTTP::Client",
- proc: ->(url : String) {
- HTTP::Client.get(url).body
- },
- }
-end
diff --git a/lib/halite/benchmarks/src/run_benchmark.cr b/lib/halite/benchmarks/src/run_benchmark.cr
deleted file mode 100644
index 8e37635a..00000000
--- a/lib/halite/benchmarks/src/run_benchmark.cr
+++ /dev/null
@@ -1,18 +0,0 @@
-require "./support/**"
-require "./clients/**"
-
-module Client
- MEMBERS = [] of NamedTuple(name: String, proc: Proc(String, String))
-end
-
-url = run_server
-
-sleep 1
-
-Benchmark.tach(10_000) do |x|
- Client::MEMBERS.each do |client|
- x.report(client["name"]) do
- client["proc"].call(url)
- end
- end
-end
diff --git a/lib/halite/benchmarks/src/support/http_server.cr b/lib/halite/benchmarks/src/support/http_server.cr
deleted file mode 100644
index 46030a97..00000000
--- a/lib/halite/benchmarks/src/support/http_server.cr
+++ /dev/null
@@ -1,16 +0,0 @@
-require "http/server"
-
-def run_server
- port = 12381
- server = HTTP::Server.new do |context|
- context.response.content_type = "text/plain"
- text = "x" * 10000
- context.response.print text
- end
-
- spawn do
- server.listen(port)
- end
-
- "http://localhost:#{port}"
-end
diff --git a/lib/halite/benchmarks/src/support/tach_benchmark.cr b/lib/halite/benchmarks/src/support/tach_benchmark.cr
deleted file mode 100644
index 1aad9d1f..00000000
--- a/lib/halite/benchmarks/src/support/tach_benchmark.cr
+++ /dev/null
@@ -1,77 +0,0 @@
-module Benchmark
- def self.tach(times : Int32)
- {% if !flag?(:release) %}
- puts "Warning: benchmarking without the `--release` flag won't yield useful results"
- {% end %}
-
- job = Tach::Job.new(times)
- yield job
- job.execute
- job.report
- job
- end
-
- module Tach
- class Job
- def initialize(@times : Int32 = 1)
- @benchmarks = [] of {String, ->}
- @results = {} of String => Float64
- end
-
- def report(label : String, &block)
- @benchmarks << {label, block}
- end
-
- def execute
- @benchmarks.each do |benchmark|
- GC.collect
-
- label, block = benchmark
- durations = [] of Float64
- @times.times do
- before = Time.utc
- block.call
- after = Time.utc
-
- durations << (after - before).total_seconds
- end
-
- average = durations.sum.to_f / @times.to_f
-
- @results[label] = average
- end
- end
-
- def report
- fastest = @results.min_by { |_, value| value }
-
- puts "Tach times: #{@times}"
- printf "%30s %20s\n", "Tach", "Total"
- @results.each do |label, result|
- mark = label == fastest.first ? " (fastest)" : ""
-
- printf "%30s %20s%s\n", label, human_mean(result), mark
- end
- end
-
- private def human_mean(iteration_time)
- case Math.log10(iteration_time)
- when 0..Float64::MAX
- digits = iteration_time
- suffix = "s"
- when -3..0
- digits = iteration_time * 1000
- suffix = "ms"
- when -6..-3
- digits = iteration_time * 1_000_000
- suffix = "ยตs"
- else
- digits = iteration_time * 1_000_000_000
- suffix = "ns"
- end
-
- "#{digits.round(4).to_s.rjust(6)}#{suffix}"
- end
- end
- end
-end
diff --git a/lib/halite/halite-logo-small.png b/lib/halite/halite-logo-small.png
deleted file mode 100644
index 3e6a65e4..00000000
Binary files a/lib/halite/halite-logo-small.png and /dev/null differ
diff --git a/lib/halite/halite-logo.png b/lib/halite/halite-logo.png
deleted file mode 100644
index 214813fd..00000000
Binary files a/lib/halite/halite-logo.png and /dev/null differ
diff --git a/lib/halite/lib b/lib/halite/lib
deleted file mode 120000
index a96aa0ea..00000000
--- a/lib/halite/lib
+++ /dev/null
@@ -1 +0,0 @@
-..
\ No newline at end of file
diff --git a/lib/halite/shard.yml b/lib/halite/shard.yml
deleted file mode 100644
index 6174cf2b..00000000
--- a/lib/halite/shard.yml
+++ /dev/null
@@ -1,9 +0,0 @@
-name: halite
-version: 0.12.0
-
-authors:
- - icyleaf
-
-crystal: ">= 0.36.1, < 2.0.0"
-
-license: MIT
diff --git a/lib/halite/src/halite.cr b/lib/halite/src/halite.cr
deleted file mode 100644
index 512e1149..00000000
--- a/lib/halite/src/halite.cr
+++ /dev/null
@@ -1,53 +0,0 @@
-require "./halite/*"
-require "./halite/ext/*"
-
-module Halite
- extend Chainable
-
- VERSION = "0.12.0"
-
- module Helper
- # Parses a `Time` into a [RFC 3339](https://tools.ietf.org/html/rfc3339) datetime format string
- # ([ISO 8601](http://xml.coverpages.org/ISO-FDIS-8601.pdf) profile).
- #
- # > Load Enviroment named "TZ" as high priority
- def self.to_rfc3339(time : Time, *, timezone = ENV["TZ"]?, fraction_digits : Int = 0)
- Time::Format::RFC_3339.format(time.in(configure_location(timezone)), fraction_digits: fraction_digits)
- end
-
- # Parses a `Time` into a [RFC 3339](https://tools.ietf.org/html/rfc3339) datetime format string to `IO`
- # ([ISO 8601](http://xml.coverpages.org/ISO-FDIS-8601.pdf) profile).
- #
- # > Load Enviroment named "TZ" as high priority
- def self.to_rfc3339(time : Time, io : IO, *, timezone = ENV["TZ"]?, fraction_digits : Int = 0)
- Time::Format::RFC_3339.format(time.in(configure_location(timezone)), io, fraction_digits)
- end
-
- # :nodoc:
- private def self.configure_location(timezone = ENV["TZ"]?)
- timezone ? Time::Location.load(timezone.not_nil!) : Time::Location::UTC
- end
- end
-
- @@features = {} of String => Feature.class
-
- module FeatureRegister
- def register_feature(name : String, klass : Feature.class)
- @@features[name] = klass
- end
-
- def feature(name : String)
- @@features[name]
- end
-
- def feature?(name : String)
- @@features[name]?
- end
-
- def has_feature?(name)
- @@features.keys.includes?(name)
- end
- end
-
- extend FeatureRegister
-end
diff --git a/lib/halite/src/halite/chainable.cr b/lib/halite/src/halite/chainable.cr
deleted file mode 100644
index 4f35d8ed..00000000
--- a/lib/halite/src/halite/chainable.cr
+++ /dev/null
@@ -1,537 +0,0 @@
-require "base64"
-
-module Halite
- module Chainable
- {% for verb in %w(get head) %}
- # {{ verb.id.capitalize }} a resource
- #
- # ```
- # Halite.{{ verb.id }}("http://httpbin.org/anything", params: {
- # first_name: "foo",
- # last_name: "bar"
- # })
- # ```
- def {{ verb.id }}(uri : String, *,
- headers : (Hash(String, _) | NamedTuple)? = nil,
- params : (Hash(String, _) | NamedTuple)? = nil,
- raw : String? = nil,
- tls : OpenSSL::SSL::Context::Client? = nil) : Halite::Response
- request({{ verb }}, uri, headers: headers, params: params, raw: raw, tls: tls)
- end
-
- # {{ verb.id.capitalize }} a streaming resource
- #
- # ```
- # Halite.{{ verb.id }}("http://httpbin.org/anything") do |response|
- # puts response.status_code
- # while line = response.body_io.gets
- # puts line
- # end
- # end
- # ```
- def {{ verb.id }}(uri : String, *,
- headers : (Hash(String, _) | NamedTuple)? = nil,
- params : (Hash(String, _) | NamedTuple)? = nil,
- raw : String? = nil,
- tls : OpenSSL::SSL::Context::Client? = nil,
- &block : Halite::Response ->)
- request({{ verb }}, uri, headers: headers, params: params, raw: raw, tls: tls, &block)
- end
- {% end %}
-
- {% for verb in %w(put post patch delete options) %}
- # {{ verb.id.capitalize }} a resource
- #
- # ### Request with form data
- #
- # ```
- # Halite.{{ verb.id }}("http://httpbin.org/anything", form: {
- # first_name: "foo",
- # last_name: "bar"
- # })
- # ```
- #
- # ### Request with json data
- #
- # ```
- # Halite.{{ verb.id }}("http://httpbin.org/anything", json: {
- # first_name: "foo",
- # last_name: "bar"
- # })
- # ```
- #
- # ### Request with raw string
- #
- # ```
- # Halite.{{ verb.id }}("http://httpbin.org/anything", raw: "name=Peter+Lee&address=%23123+Happy+Ave&Language=C%2B%2B")
- # ```
- def {{ verb.id }}(uri : String, *,
- headers : (Hash(String, _) | NamedTuple)? = nil,
- params : (Hash(String, _) | NamedTuple)? = nil,
- form : (Hash(String, _) | NamedTuple)? = nil,
- json : (Hash(String, _) | NamedTuple)? = nil,
- raw : String? = nil,
- tls : OpenSSL::SSL::Context::Client? = nil) : Halite::Response
- request({{ verb }}, uri, headers: headers, params: params, form: form, json: json, raw: raw, tls: tls)
- end
-
- # {{ verb.id.capitalize }} a streaming resource
- #
- # ```
- # Halite.{{ verb.id }}("http://httpbin.org/anything") do |response|
- # puts response.status_code
- # while line = response.body_io.gets
- # puts line
- # end
- # end
- # ```
- def {{ verb.id }}(uri : String, *,
- headers : (Hash(String, _) | NamedTuple)? = nil,
- params : (Hash(String, _) | NamedTuple)? = nil,
- form : (Hash(String, _) | NamedTuple)? = nil,
- json : (Hash(String, _) | NamedTuple)? = nil,
- raw : String? = nil,
- tls : OpenSSL::SSL::Context::Client? = nil,
- &block : Halite::Response ->)
- request({{ verb }}, uri, headers: headers, params: params, form: form, json: json, raw: raw, tls: tls, &block)
- end
- {% end %}
-
- # Adds a endpoint to the request.
- #
- #
- # ```
- # Halite.endpoint("https://httpbin.org")
- # .get("/get")
- # ```
- def endpoint(endpoint : String | URI) : Halite::Client
- branch(default_options.with_endpoint(endpoint))
- end
-
- # Make a request with the given Basic authorization header
- #
- # ```
- # Halite.basic_auth("icyleaf", "p@ssw0rd")
- # .get("http://httpbin.org/get")
- # ```
- #
- # See Also: [http://tools.ietf.org/html/rfc2617](http://tools.ietf.org/html/rfc2617)
- def basic_auth(user : String, pass : String) : Halite::Client
- auth("Basic " + Base64.strict_encode(user + ":" + pass))
- end
-
- # Make a request with the given Authorization header
- #
- # ```
- # Halite.auth("private-token", "6abaef100b77808ceb7fe26a3bcff1d0")
- # .get("http://httpbin.org/get")
- # ```
- def auth(value : String) : Halite::Client
- headers({"Authorization" => value})
- end
-
- # Accept the given MIME type
- #
- # ```
- # Halite.accept("application/json")
- # .get("http://httpbin.org/get")
- # ```
- def accept(value : String) : Halite::Client
- headers({"Accept" => value})
- end
-
- # Set requests user agent
- #
- # ```
- # Halite.user_agent("Custom User Agent")
- # .get("http://httpbin.org/get")
- # ```
- def user_agent(value : String) : Halite::Client
- headers({"User-Agent" => value})
- end
-
- # Make a request with the given headers
- #
- # ```
- # Halite.headers({"Content-Type", "application/json", "Connection": "keep-alive"})
- # .get("http://httpbin.org/get")
- # # Or
- # Halite.headers({content_type: "application/json", connection: "keep-alive"})
- # .get("http://httpbin.org/get")
- # ```
- def headers(headers : Hash(String, _) | NamedTuple) : Halite::Client
- branch(default_options.with_headers(headers))
- end
-
- # Make a request with the given headers
- #
- # ```
- # Halite.headers(content_type: "application/json", connection: "keep-alive")
- # .get("http://httpbin.org/get")
- # ```
- def headers(**kargs) : Halite::Client
- branch(default_options.with_headers(kargs))
- end
-
- # Make a request with the given cookies
- #
- # ```
- # Halite.cookies({"private-token", "6abaef100b77808ceb7fe26a3bcff1d0"})
- # .get("http://httpbin.org/get")
- # # Or
- # Halite.cookies({private-token: "6abaef100b77808ceb7fe26a3bcff1d0"})
- # .get("http://httpbin.org/get")
- # ```
- def cookies(cookies : Hash(String, _) | NamedTuple) : Halite::Client
- branch(default_options.with_cookies(cookies))
- end
-
- # Make a request with the given cookies
- #
- # ```
- # Halite.cookies(name: "icyleaf", "gender": "male")
- # .get("http://httpbin.org/get")
- # ```
- def cookies(**kargs) : Halite::Client
- branch(default_options.with_cookies(kargs))
- end
-
- # Make a request with the given cookies
- #
- # ```
- # cookies = HTTP::Cookies.from_client_headers(headers)
- # Halite.cookies(cookies)
- # .get("http://httpbin.org/get")
- # ```
- def cookies(cookies : HTTP::Cookies) : Halite::Client
- branch(default_options.with_cookies(cookies))
- end
-
- # Adds a timeout to the request.
- #
- # How long to wait for the server to send data before giving up, as a int, float or time span.
- # The timeout value will be applied to both the connect and the read timeouts.
- #
- # Set `nil` to timeout to ignore timeout.
- #
- # ```
- # Halite.timeout(5.5).get("http://httpbin.org/get")
- # # Or
- # Halite.timeout(2.minutes)
- # .post("http://httpbin.org/post", form: {file: "file.txt"})
- # ```
- def timeout(timeout : (Int32 | Float64 | Time::Span)?)
- timeout ? timeout(timeout, timeout, timeout) : branch
- end
-
- # Adds a timeout to the request.
- #
- # How long to wait for the server to send data before giving up, as a int, float or time span.
- # The timeout value will be applied to both the connect and the read timeouts.
- #
- # ```
- # Halite.timeout(3, 3.minutes, 5)
- # .post("http://httpbin.org/post", form: {file: "file.txt"})
- # # Or
- # Halite.timeout(3.04, 64, 10.0)
- # .get("http://httpbin.org/get")
- # ```
- def timeout(connect : (Int32 | Float64 | Time::Span)? = nil,
- read : (Int32 | Float64 | Time::Span)? = nil,
- write : (Int32 | Float64 | Time::Span)? = nil)
- branch(default_options.with_timeout(connect, read, write))
- end
-
- # Returns `Options` self with automatically following redirects.
- #
- # ```
- # # Automatically following redirects.
- # Halite.follow
- # .get("http://httpbin.org/relative-redirect/5")
- #
- # # Always redirect with any request methods
- # Halite.follow(strict: false)
- # .get("http://httpbin.org/get")
- # ```
- def follow(strict = Halite::Options::Follow::STRICT) : Halite::Client
- branch(default_options.with_follow(strict: strict))
- end
-
- # Returns `Options` self with given max hops of redirect times.
- #
- # ```
- # # Max hops 3 times
- # Halite.follow(3)
- # .get("http://httpbin.org/relative-redirect/3")
- #
- # # Always redirect with any request methods
- # Halite.follow(4, strict: false)
- # .get("http://httpbin.org/relative-redirect/4")
- # ```
- def follow(hops : Int32, strict = Halite::Options::Follow::STRICT) : Halite::Client
- branch(default_options.with_follow(hops, strict))
- end
-
- # Returns `Options` self with enable or disable logging.
- #
- # #### Enable logging
- #
- # Same as call `logging` method without any argument.
- #
- # ```
- # Halite.logging.get("http://httpbin.org/get")
- # ```
- #
- # #### Disable logging
- #
- # ```
- # Halite.logging(false).get("http://httpbin.org/get")
- # ```
- def logging(enable : Bool = true)
- options = default_options
- options.logging = enable
- branch(options)
- end
-
- # Returns `Options` self with given the logging which it integration from `Halite::Logging`.
- #
- # #### Simple logging
- #
- # ```
- # Halite.logging
- # .get("http://httpbin.org/get", params: {name: "foobar"})
- #
- # => 2018-08-28 14:33:19 +08:00 | request | POST | http://httpbin.org/post
- # => 2018-08-28 14:33:21 +08:00 | response | 200 | http://httpbin.org/post | 1.61s | application/json
- # { ... }
- # ```
- #
- # #### Logger configuration
- #
- # By default, Halite will logging all outgoing HTTP requests and their responses(without binary stream) to `STDOUT` on DEBUG level.
- # You can configuring the following options:
- #
- # - `skip_request_body`: By default is `false`.
- # - `skip_response_body`: By default is `false`.
- # - `skip_benchmark`: Display elapsed time, by default is `false`.
- # - `colorize`: Enable colorize in terminal, only apply in `common` format, by default is `true`.
- #
- # ```
- # Halite.logging(skip_request_body: true, skip_response_body: true)
- # .post("http://httpbin.org/get", form: {image: File.open("halite-logo.png")})
- #
- # # => 2018-08-28 14:33:19 +08:00 | request | POST | http://httpbin.org/post
- # # => 2018-08-28 14:33:21 +08:00 | response | 200 | http://httpbin.org/post | 1.61s | application/json
- # ```
- #
- # #### Use custom logging
- #
- # Creating the custom logging by integration `Halite::Logging::Abstract` abstract class.
- # Here has two methods must be implement: `#request` and `#response`.
- #
- # ```
- # class CustomLogger < Halite::Logging::Abstract
- # def request(request)
- # @logger.info "| >> | %s | %s %s" % [request.verb, request.uri, request.body]
- # end
- #
- # def response(response)
- # @logger.info "| << | %s | %s %s" % [response.status_code, response.uri, response.content_type]
- # end
- # end
- #
- # # Add to adapter list (optional)
- # Halite::Logging.register_adapter "custom", CustomLogger.new
- #
- # Halite.logging(logging: CustomLogger.new)
- # .get("http://httpbin.org/get", params: {name: "foobar"})
- #
- # # We can also call it use format name if you added it.
- # Halite.logging(format: "custom")
- # .get("http://httpbin.org/get", params: {name: "foobar"})
- #
- # # => 2017-12-13 16:40:13 +08:00 | >> | GET | http://httpbin.org/get?name=foobar
- # # => 2017-12-13 16:40:15 +08:00 | << | 200 | http://httpbin.org/get?name=foobar application/json
- # ```
- def logging(logging : Halite::Logging::Abstract = Halite::Logging::Common.new)
- branch(default_options.with_logging(logging))
- end
-
- # Returns `Options` self with given the file with the path.
- #
- # #### JSON-formatted logging
- #
- # ```
- # Halite.logging(format: "json")
- # .get("http://httpbin.org/get", params: {name: "foobar"})
- # ```
- #
- # #### create a http request and log to file
- #
- # ```
- # Log.setup("halite.file", backend: Log::IOBackend.new(File.open("/tmp/halite.log", "a")))
- # Halite.logging(for: "halite.file")
- # .get("http://httpbin.org/get", params: {name: "foobar"})
- # ```
- #
- # #### Always create new log file and store data to JSON formatted
- #
- # ```
- # Log.setup("halite.file", backend: Log::IOBackend.new(File.open("/tmp/halite.log", "w"))
- # Halite.logging(for: "halite.file", format: "json")
- # .get("http://httpbin.org/get", params: {name: "foobar"})
- # ```
- #
- # Check the log file content: **/tmp/halite.log**
- def logging(format : String = "common", *, for : String = "halite",
- skip_request_body = false, skip_response_body = false,
- skip_benchmark = false, colorize = true)
- opts = {
- for: for,
- skip_request_body: skip_request_body,
- skip_response_body: skip_response_body,
- skip_benchmark: skip_benchmark,
- colorize: colorize,
- }
- branch(default_options.with_logging(format, **opts))
- end
-
- # Turn on given features and its options.
- #
- # Available features to review all subclasses of `Halite::Feature`.
- #
- # #### Use JSON logging
- #
- # ```
- # Halite.use("logging", format: "json")
- # .get("http://httpbin.org/get", params: {name: "foobar"})
- #
- # # => { ... }
- # ```
- #
- # #### Use common format logging and skip response body
- # ```
- # Halite.use("logging", format: "common", skip_response_body: true)
- # .get("http://httpbin.org/get", params: {name: "foobar"})
- #
- # # => 2018-08-28 14:58:26 +08:00 | request | GET | http://httpbin.org/get
- # # => 2018-08-28 14:58:27 +08:00 | response | 200 | http://httpbin.org/get | 615.8ms | application/json
- # ```
- def use(feature : String, **opts)
- branch(default_options.with_features(feature, **opts))
- end
-
- # Turn on given the name of features.
- #
- # Available features to review all subclasses of `Halite::Feature`.
- #
- # ```
- # Halite.use("logging", "your-custom-feature-name")
- # .get("http://httpbin.org/get", params: {name: "foobar"})
- # ```
- def use(*features)
- branch(default_options.with_features(*features))
- end
-
- # Make an HTTP request with the given verb
- #
- # ```
- # Halite.request("get", "http://httpbin.org/get", {
- # "headers" = { "user_agent" => "halite" },
- # "params" => { "nickname" => "foo" },
- # "form" => { "username" => "bar" },
- # })
- # ```
- def request(verb : String, uri : String, *,
- headers : (Hash(String, _) | NamedTuple)? = nil,
- params : (Hash(String, _) | NamedTuple)? = nil,
- form : (Hash(String, _) | NamedTuple)? = nil,
- json : (Hash(String, _) | NamedTuple)? = nil,
- raw : String? = nil,
- tls : OpenSSL::SSL::Context::Client? = nil) : Halite::Response
- request(verb, uri, options_with(headers, params, form, json, raw, tls))
- end
-
- # Make an HTTP request with the given verb and options
- #
- # > This method will be executed with oneshot request.
- #
- # ```
- # Halite.request("get", "http://httpbin.org/stream/3", headers: {"user-agent" => "halite"}) do |response|
- # puts response.status_code
- # while line = response.body_io.gets
- # puts line
- # end
- # end
- # ```
- def request(verb : String, uri : String, *,
- headers : (Hash(String, _) | NamedTuple)? = nil,
- params : (Hash(String, _) | NamedTuple)? = nil,
- form : (Hash(String, _) | NamedTuple)? = nil,
- json : (Hash(String, _) | NamedTuple)? = nil,
- raw : String? = nil,
- tls : OpenSSL::SSL::Context::Client? = nil,
- &block : Halite::Response ->)
- request(verb, uri, options_with(headers, params, form, json, raw, tls), &block)
- end
-
- # Make an HTTP request with the given verb and options
- #
- # > This method will be executed with oneshot request.
- #
- # ```
- # Halite.request("get", "http://httpbin.org/get", Halite::Options.new(
- # "headers" = { "user_agent" => "halite" },
- # "params" => { "nickname" => "foo" },
- # "form" => { "username" => "bar" },
- # )
- # ```
- def request(verb : String, uri : String, options : Halite::Options? = nil) : Halite::Response
- branch(options).request(verb, uri)
- end
-
- # Make an HTTP request with the given verb and options
- #
- # > This method will be executed with oneshot request.
- #
- # ```
- # Halite.request("get", "http://httpbin.org/stream/3") do |response|
- # puts response.status_code
- # while line = response.body_io.gets
- # puts line
- # end
- # end
- # ```
- def request(verb : String, uri : String, options : Halite::Options? = nil, &block : Halite::Response ->)
- branch(options).request(verb, uri, &block)
- end
-
- private def branch(options : Halite::Options? = nil) : Halite::Client
- options ||= default_options
- Halite::Client.new(options)
- end
-
- # Use with new instance of Halite::Client to load unique options
- #
- # Note: append options in Halite::Client#initialize and revoke at #finalize
- DEFAULT_OPTIONS = {} of UInt64 => Halite::Options
-
- private def default_options
- {% if @type.superclass %}
- DEFAULT_OPTIONS[object_id]
- {% else %}
- Halite::Options.new
- {% end %}
- end
-
- private def options_with(headers : (Hash(String, _) | NamedTuple)? = nil,
- params : (Hash(String, _) | NamedTuple)? = nil,
- form : (Hash(String, _) | NamedTuple)? = nil,
- json : (Hash(String, _) | NamedTuple)? = nil,
- raw : String? = nil,
- tls : OpenSSL::SSL::Context::Client? = nil)
- options = Halite::Options.new(headers: headers, params: params, form: form, json: json, raw: raw, tls: tls)
- default_options.merge!(options)
- end
- end
-end
diff --git a/lib/halite/src/halite/client.cr b/lib/halite/src/halite/client.cr
deleted file mode 100644
index aa588cd7..00000000
--- a/lib/halite/src/halite/client.cr
+++ /dev/null
@@ -1,304 +0,0 @@
-require "./request"
-require "./response"
-require "./redirector"
-
-require "http/client"
-require "json"
-
-module Halite
- # Clients make requests and receive responses
- #
- # Support all `Chainable` methods.
- #
- # ### Simple setup
- #
- # ```
- # client = Halite::Client.new(headers: {
- # "private-token" => "bdf39d82661358f80b31b67e6f89fee4"
- # })
- #
- # client.auth(private_token: "bdf39d82661358f80b31b67e6f89fee4").
- # .get("http://httpbin.org/get", params: {
- # name: "icyleaf"
- # })
- # ```
- #
- # ### Setup with block
- #
- # ```
- # client = Halite::Client.new do
- # basic_auth "name", "foo"
- # headers content_type: "application/jsong"
- # read_timeout 3.minutes
- # logging true
- # end
- # ```
- class Client
- include Chainable
-
- # Instance a new client
- #
- # ```
- # Halite::Client.new(headers: {"private-token" => "bdf39d82661358f80b31b67e6f89fee4"})
- # ```
- def self.new(*,
- endpoint : (String | URI)? = nil,
- headers : (Hash(String, _) | NamedTuple)? = nil,
- cookies : (Hash(String, _) | NamedTuple)? = nil,
- params : (Hash(String, _) | NamedTuple)? = nil,
- form : (Hash(String, _) | NamedTuple)? = nil,
- json : (Hash(String, _) | NamedTuple)? = nil,
- raw : String? = nil,
- timeout = Timeout.new,
- follow = Follow.new,
- tls : OpenSSL::SSL::Context::Client? = nil)
- new(Options.new(
- endpoint: endpoint,
- headers: headers,
- cookies: cookies,
- params: params,
- form: form,
- json: json,
- raw: raw,
- tls: tls,
- timeout: timeout,
- follow: follow
- ))
- end
-
- property options
-
- # Instance a new client with block
- #
- # ```
- # client = Halite::Client.new do
- # basic_auth "name", "foo"
- # logging true
- # end
- # ```
- def self.new(&block)
- instance = new
- yield_instance = with instance yield
- if yield_instance
- yield_instance.options.merge!(yield_instance.oneshot_options)
- yield_instance.oneshot_options.clear!
- instance = yield_instance
- end
-
- instance
- end
-
- # Instance a new client
- #
- # ```
- # options = Halite::Options.new(headers: {
- # "private-token" => "bdf39d82661358f80b31b67e6f89fee4",
- # })
- #
- # client = Halite::Client.new(options)
- # ```
- def initialize(@options = Halite::Options.new)
- @history = [] of Response
-
- DEFAULT_OPTIONS[object_id] = Halite::Options.new
- end
-
- def finalize
- DEFAULT_OPTIONS.delete(object_id)
- end
-
- # Make an HTTP request
- def request(verb : String, uri : String, options : Halite::Options? = nil) : Halite::Response
- opts = options ? @options.merge(options.not_nil!) : @options
- request = build_request(verb, uri, opts)
- response = perform_chain(request, opts) do
- perform(request, opts)
- end
-
- return response if opts.follow.hops.zero?
-
- Redirector.new(request, response, opts).perform do |req|
- perform(req, opts)
- end
- end
-
- # Make an HTTP request
- def request(verb : String, uri : String, options : Halite::Options? = nil, &block : Halite::Response ->)
- opts = options ? @options.merge(options.not_nil!) : @options
- request = build_request(verb, uri, opts)
- perform(request, opts, &block)
- end
-
- # Find interceptor and return `Response` else perform HTTP request.
- private def perform_chain(request : Halite::Request, options : Halite::Options, &block : -> Response)
- chain = Feature::Chain.new(request, nil, options, &block)
- options.features.each do |_, feature|
- current_chain = feature.intercept(chain)
- if current_chain.result == Feature::Chain::Result::Next
- chain = current_chain
- elsif current_chain.result == Feature::Chain::Result::Return && (response = current_chain.response)
- return handle_response(response, options)
- end
- end
-
- # Make sure return if has response with each interceptor
- if response = chain.response
- return handle_response(response, options)
- end
-
- # Perform original HTTP request if not found any response in interceptors
- block.call
- end
-
- # Perform a single (no follow) HTTP request
- private def perform(request : Halite::Request, options : Halite::Options) : Halite::Response
- raise RequestError.new("SSL context given for HTTP URI = #{request.uri}") if request.scheme == "http" && options.tls
-
- conn = make_connection(request, options)
- conn_response = conn.exec(request.verb, request.full_path, request.headers, request.body)
- handle_response(request, conn_response, options)
- rescue ex : IO::TimeoutError
- raise TimeoutError.new(ex.message)
- rescue ex : Socket::Error
- raise ConnectionError.new(ex.message)
- end
-
- # Perform a single (no follow) streaming HTTP request and redirect automatically
- private def perform(request : Halite::Request, options : Halite::Options, &block : Halite::Response ->)
- raise RequestError.new("SSL context given for HTTP URI = #{request.uri}") if request.scheme == "http" && options.tls
-
- conn = make_connection(request, options)
- conn.exec(request.verb, request.full_path, request.headers, request.body) do |conn_response|
- response = handle_response(request, conn_response, options)
- redirector = Redirector.new(request, response, options)
- if redirector.avaiable?
- redirector.each_redirect do |req|
- perform(req, options, &block)
- end
- else
- block.call(response)
- end
-
- return response
- end
- end
-
- # Prepare a HTTP request
- private def build_request(verb : String, uri : String, options : Halite::Options) : Halite::Request
- uri = make_request_uri(uri, options)
- body_data = make_request_body(options)
- headers = make_request_headers(options, body_data.content_type)
- request = Request.new(verb, uri, headers, body_data.body)
-
- # reset options during onshot request, see `default_options` method at the bottom of file.
- default_options.clear!
-
- options.features.reduce(request) do |req, (_, feature)|
- feature.request(req)
- end
- end
-
- # Merges query params if needed
- private def make_request_uri(url : String, options : Halite::Options) : URI
- uri = resolve_uri(url, options)
- if params = options.params
- query = HTTP::Params.encode(params)
- uri.query = [uri.query, query].compact.join('&') unless query.empty?
- end
-
- uri
- end
-
- # Merges request headers
- private def make_request_headers(options : Halite::Options, content_type : String?) : HTTP::Headers
- headers = options.headers
- if (value = content_type) && !value.empty? && !headers.has_key?("Content-Type")
- headers.add("Content-Type", value)
- end
-
- # Cookie shards
- options.cookies.add_request_headers(headers)
- end
-
- # Create the request body object to send
- private def make_request_body(options : Halite::Options) : Halite::Request::Data
- if (form = options.form) && !form.empty?
- FormData.create(form)
- elsif (hash = options.json) && !hash.empty?
- body = JSON.build do |builder|
- hash.to_json(builder)
- end
-
- Halite::Request::Data.new(body, "application/json")
- elsif (raw = options.raw) && !raw.empty?
- Halite::Request::Data.new(raw, "text/plain")
- else
- Halite::Request::Data.new("")
- end
- end
-
- # Create the http connection
- private def make_connection(request, options)
- conn = HTTP::Client.new(request.domain, options.tls)
- conn.connect_timeout = options.timeout.connect.not_nil! if options.timeout.connect
- conn.read_timeout = options.timeout.read.not_nil! if options.timeout.read
- conn.write_timeout = options.timeout.write.not_nil! if options.timeout.write
- conn
- end
-
- # Convert HTTP::Client::Response to response and handles response (see below)
- private def handle_response(request, conn_response : HTTP::Client::Response, options) : Halite::Response
- response = Response.new(uri: request.uri, conn: conn_response, history: @history)
- handle_response(response, options)
- end
-
- # Handles response by reduce the response of feature, add history and update options
- private def handle_response(response, options) : Halite::Response
- response = options.features.reduce(response) do |res, (_, feature)|
- feature.response(res)
- end
-
- # Append history of response if enable follow
- @history << response unless options.follow.hops.zero?
- store_cookies_from_response(response)
- end
-
- # Store cookies for sessions use from response
- private def store_cookies_from_response(response : Halite::Response) : Halite::Response
- return response unless response.headers
-
- @options.with_cookies(HTTP::Cookies.from_server_headers(response.headers))
- response
- end
-
- # Use in instance/session mode, it will replace same method in `Halite::Chainable`.
- private def branch(options : Halite::Options? = nil) : Halite::Client
- oneshot_options.merge!(options)
- self
- end
-
- private def resolve_uri(url : String, options : Halite::Options) : URI
- return URI.parse(url) unless endpoint = options.endpoint
- return endpoint if url.empty?
-
- endpoint.path += '/' unless endpoint.path.ends_with?('/')
- endpoint.resolve(url)
- end
-
- # :nodoc:
- @oneshot_options : Halite::Options?
-
- # :nodoc:
- #
- # Store options on each request, then it will reset after finish response.
- #
- # > It will called in this class method, so it mark be public but not recommend to users.
- #
- # It make sure never store any gived headers, cookies, query, form, raw and tls
- # during each request in instance/session mode.
- def oneshot_options
- @oneshot_options ||= Halite::Options.new
- @oneshot_options.not_nil!
- end
- end
-end
diff --git a/lib/halite/src/halite/error.cr b/lib/halite/src/halite/error.cr
deleted file mode 100644
index 3e489c69..00000000
--- a/lib/halite/src/halite/error.cr
+++ /dev/null
@@ -1,87 +0,0 @@
-module Halite
- module Exception
- # Generic error
- class Error < ::Exception; end
-
- # Generic Connection error
- class ConnectionError < Error; end
-
- # Generic Request error
- class RequestError < Error; end
-
- # Generic Response error
- class ResponseError < Error; end
-
- # Generic Feature error
- class FeatureError < Error; end
-
- # The method given was not understood
- class UnsupportedMethodError < RequestError; end
-
- # The scheme given was not understood
- class UnsupportedSchemeError < RequestError; end
-
- # The head method can not streaming without empty response
- class UnsupportedStreamMethodError < RequestError; end
-
- # Requested to do something when we're in the wrong state
- class StateError < RequestError; end
-
- # Generic Timeout error
- class TimeoutError < RequestError; end
-
- # The feature given was not understood
- class UnRegisterFeatureError < FeatureError; end
-
- # The format given was not understood
- class UnRegisterLoggerFormatError < FeatureError; end
-
- # Notifies that we reached max allowed redirect hops
- class TooManyRedirectsError < ResponseError; end
-
- # Notifies that following redirects got into an endless loop
- class EndlessRedirectError < TooManyRedirectsError; end
-
- # The MIME type(adapter) given was not understood
- class UnRegisterMimeTypeError < ResponseError; end
-
- # Generic API error
- class APIError < ResponseError
- getter uri
- getter status_code
- getter status_message : String? = nil
-
- def initialize(@message : String? = nil, @status_code : Int32? = nil, @uri : URI? = nil)
- @status_message = build_status_message
- if status_code = @status_code
- @message ||= "#{status_code} #{@status_message}"
- end
-
- super(@message)
- end
-
- private def build_status_message : String
- String::Builder.build do |io|
- if status_code = @status_code
- io << "#{HTTP::Status.new(status_code).description.to_s.downcase} error"
- else
- io << "#{@message || "unknown"} error"
- end
-
- io << " with url: #{@uri}" if uri = @uri
- end.to_s
- end
- end
-
- # 4XX client error
- class ClientError < APIError; end
-
- # 5XX server error
- class ServerError < APIError; end
- end
-
- {% for cls in Exception.constants %}
- # :nodoc:
- alias {{ cls.id }} = Exception::{{ cls.id }}
- {% end %}
-end
diff --git a/lib/halite/src/halite/ext/file_to_json.cr b/lib/halite/src/halite/ext/file_to_json.cr
deleted file mode 100644
index 7e1e54a6..00000000
--- a/lib/halite/src/halite/ext/file_to_json.cr
+++ /dev/null
@@ -1,6 +0,0 @@
-# :nodoc:
-class File
- def to_json(json : JSON::Builder)
- json.string(to_s)
- end
-end
diff --git a/lib/halite/src/halite/ext/http_headers_encode.cr b/lib/halite/src/halite/ext/http_headers_encode.cr
deleted file mode 100644
index fdccd745..00000000
--- a/lib/halite/src/halite/ext/http_headers_encode.cr
+++ /dev/null
@@ -1,57 +0,0 @@
-module HTTP
- # This is **extension** apply in Halite.
- struct Headers
- # Returns the given key value pairs as HTTP Headers
- #
- # Every parameter added is directly written to an IO, where keys are properly escaped.
- #
- # ```
- # HTTP::Headers.encode({
- # content_type: "application/json",
- # })
- # # => "HTTP::Headers{"Content-Type" => "application/json"}"
- #
- # HTTP::Headers.encode({
- # "conTENT-type": "application/json",
- # })
- # # => "HTTP::Headers{"Content-Type" => "application/json"}"
- # ```
- def self.encode(data : Hash(String, _) | NamedTuple) : HTTP::Headers
- ::HTTP::Headers.new.tap do |builder|
- data = data.is_a?(NamedTuple) ? data.to_h : data
- data.each do |key, value|
- key = key.to_s.gsub("_", "-").split("-").map { |v| v.capitalize }.join("-")
- # skip invalid value of content length
- next if key == "Content-Length" && !(value =~ /^\d+$/)
-
- builder.add key, value.is_a?(Array(String)) ? value : value.to_s
- end
- end
- end
-
- # Same as `#encode`
- def self.encode(**data)
- encode(data)
- end
-
- # Similar as `Hahs#to_h` but return `String` if it has one value of the key.
- #
- # ```
- # headers = HTTP::Headers{"Accepts" => ["application/json", "text/html"], "Content-Type" => ["text/html"]}
- # headers["Accepts"] # => ["application/json", "text/html"]
- # headers["Content-Type"] # => "text/html"
- # ```
- def to_flat_h
- @hash.each_with_object({} of String => String | Array(String)) do |(key, values), obj|
- obj[key.name] = case values
- when String
- values.as(String)
- when Array
- values.size == 1 ? values[0].as(String) : values.as(Array(String))
- else
- raise Halite::Error.new("Not support type `#{values.class} with value: #{values}")
- end
- end
- end
- end
-end
diff --git a/lib/halite/src/halite/ext/http_params_encode.cr b/lib/halite/src/halite/ext/http_params_encode.cr
deleted file mode 100644
index 5dcde8cd..00000000
--- a/lib/halite/src/halite/ext/http_params_encode.cr
+++ /dev/null
@@ -1,79 +0,0 @@
-module HTTP
- # This is **extension** apply in Halite.
- struct Params
- # Returns the given key value pairs as a url-encoded query.
- #
- # Every parameter added is directly written to an IO, where keys and values are properly escaped.
- #
- # ```
- # HTTP::Params.encode({
- # "name" => "Lizeth Gusikowski",
- # "skill" => ["ruby", "crystal"],
- # "company" => {
- # "name" => "Keeling Inc",
- # },
- # "avatar" => File.open("avatar_big.png"),
- # })
- # # => "name=Lizeth+Gusikowski&skill=ruby&skill=crystal&company=%7B%22name%22+%3D%3E+%22Keeling+Inc%22%7D&avatar=avatar_big.png"
- # ```
- def self.encode(hash : Hash) : String
- ::HTTP::Params.build do |form|
- hash.each do |key, value|
- key = key.to_s
- case value
- when Array
- value.each do |item|
- form.add("#{key}", item.to_s)
- end
- when File
- form.add(key, value.as(File).path)
- when Hash
- value.each do |hkey, hvalue|
- form.add("#{key}[#{hkey}]", hvalue.to_s)
- end
- else
- form.add(key, value.to_s)
- end
- end
- end
- end
-
- # Returns the given key value pairs as a url-encoded query.
- #
- # Every parameter added is directly written to an IO, where keys and values are properly escaped.
- #
- # ```
- # HTTP::Params.encode({
- # name: "Lizeth Gusikowski",
- # skill: ["ruby", "crystal"],
- # company: {
- # name: "Keeling Inc",
- # },
- # avatar: File.open("avatar_big.png"
- # })
- # # => "name=Lizeth+Gusikowski&skill=ruby&skill=crystal&company=%7B%22name%22+%3D%3E+%22Keeling+Inc%22%7D&avatar=avatar_big.png"
- # ```
- def self.encode(named_tuple : NamedTuple) : String
- encode(named_tuple.to_h)
- end
-
- # Returns the given key value pairs as a url-encoded query.
- #
- # Every parameter added is directly written to an IO, where keys and values are properly escaped.
- #
- # ```
- # HTTP::Params.encode(
- # name: "Lizeth Gusikowski",
- # skill: ["ruby", "crystal"],
- # company: {
- # name: "Keeling Inc",
- # },
- # avatar: File.open("avatar_big.png"
- # )
- # # => "name=Lizeth+Gusikowski&skill=ruby&skill=crystal&company=%7B%22name%22+%3D%3E+%22Keeling+Inc%22%7D&avatar=avatar_big.png"
- # ```
- def self.encode(**named_tuple) : String
- encode(named_tuple)
- end
- end
-end
diff --git a/lib/halite/src/halite/feature.cr b/lib/halite/src/halite/feature.cr
deleted file mode 100644
index fcfc6297..00000000
--- a/lib/halite/src/halite/feature.cr
+++ /dev/null
@@ -1,71 +0,0 @@
-module Halite
- abstract class Feature
- def initialize(**options)
- end
-
- # Cooks with request
- def request(request : Halite::Request) : Halite::Request
- request
- end
-
- # Cooking with response
- def response(response : Halite::Response) : Halite::Response
- response
- end
-
- # Intercept and cooking request and response
- def intercept(chain : Halite::Feature::Chain) : Halite::Feature::Chain
- chain
- end
-
- # Feature chain
- #
- # Chain has two result:
- #
- # next: perform and run next interceptor
- # return: perform and return
- class Chain
- enum Result
- Next
- Return
- end
-
- property request
- getter response
- getter result
-
- @performed_response : Halite::Response?
-
- def initialize(@request : Halite::Request, @response : Halite::Response?, @options : Halite::Options, &block : -> Halite::Response)
- @result = Result::Next
- @performed_response = nil
- @perform_request_block = block
- end
-
- def next(response)
- @result = Result::Next
- @response = response
-
- self
- end
-
- def return(response)
- @result = Result::Return
- @response = response
-
- self
- end
-
- def performed?
- !@performed_response.nil?
- end
-
- def perform
- @performed_response ||= @perform_request_block.call
- @performed_response.not_nil!
- end
- end
- end
-end
-
-require "./features/*"
diff --git a/lib/halite/src/halite/features/cache.cr b/lib/halite/src/halite/features/cache.cr
deleted file mode 100644
index 703732b4..00000000
--- a/lib/halite/src/halite/features/cache.cr
+++ /dev/null
@@ -1,184 +0,0 @@
-require "json"
-require "digest"
-require "file_utils"
-
-module Halite
- # Cache feature use for caching HTTP response to local storage to speed up in developing stage.
- #
- # It has the following options:
- #
- # - `file`: Load cache from file. it conflict with `path` and `expires`.
- # - `path`: The path of cache, default is "/tmp/halite/cache/"
- # - `expires`: The expires time of cache, default is never expires.
- # - `debug`: The debug mode of cache, default is `true`
- #
- # With debug mode, cached response it always included some headers information:
- #
- # - `X-Halite-Cached-From`: Cache source (cache or file)
- # - `X-Halite-Cached-Key`: Cache key with verb, uri and body (return with cache, not `file` passed)
- # - `X-Halite-Cached-At`: Cache created time
- # - `X-Halite-Cached-Expires-At`: Cache expired time (return with cache, not `file` passed)
- #
- # ```
- # Halite.use("cache").get "http://httpbin.org/anything" # request a HTTP
- # r = Halite.use("cache").get "http://httpbin.org/anything" # request from local storage
- # r.headers # => {..., "X-Halite-Cached-At" => "2018-08-30 10:41:14 UTC", "X-Halite-Cached-By" => "Halite", "X-Halite-Cached-Expires-At" => "2018-08-30 10:41:19 UTC", "X-Halite-Cached-Key" => "2bb155e6c8c47627da3d91834eb4249a"}}
- # ```
- class Cache < Feature
- DEFAULT_PATH = "/tmp/halite/cache/"
-
- getter file : String?
- getter path : String
- getter expires : Time::Span?
- getter debug : Bool
-
- # return a new Cache instance
- #
- # Accepts argument:
- #
- # - **debug**: `Bool`
- # - **path**: `String`
- # - **expires**: `(Int32 | Time::Span)?`
- def initialize(**options)
- @debug = options.fetch(:debug, true).as(Bool)
- if file = options[:file]?
- @file = file
- @path = DEFAULT_PATH
- @expires = nil
- else
- @file = nil
- @path = options.fetch(:path, DEFAULT_PATH).as(String)
- @expires = case expires = options[:expires]?
- when Time::Span
- expires.as(Time::Span)
- when Int32
- Time::Span.new(seconds: expires.as(Int32), nanoseconds: 0)
- when Nil
- nil
- else
- raise "Only accept Int32 and Time::Span type."
- end
- end
- end
-
- def intercept(chain)
- response = cache(chain) do
- chain.perform
- end
-
- chain.return(response)
- end
-
- private def cache(chain, &block : -> Halite::Response)
- if response = find_cache(chain.request)
- return response
- end
-
- response = yield
- write_cache(chain.request, response)
- response
- end
-
- private def find_cache(request : Halite::Request) : Halite::Response?
- if file = @file
- build_response(request, file)
- elsif response = build_response(request)
- response
- end
- end
-
- private def find_file(file) : Halite::Response
- raise Error.new("Not find cache file: #{file}") if File.file?(file)
- build_response(file)
- end
-
- private def build_response(request : Halite::Request, file : String? = nil) : Halite::Response?
- status_code = 200
- headers = HTTP::Headers.new
- cache_from = "file"
-
- unless file
- # Cache in path
- key = generate_cache_key(request)
- path = File.join(@path, key)
-
- return unless Dir.exists?(path)
-
- cache_from = "cache"
- cache_file = File.join(path, "#{key}.cache")
- if File.file?(cache_file) && !cache_expired?(cache_file)
- file = cache_file
-
- if metadata = find_metadata(path)
- status_code = metadata["status_code"].as_i
- metadata["headers"].as_h.each do |name, value|
- headers[name] = value.as_s
- end
- end
-
- if @debug
- headers["X-Halite-Cached-Key"] = key
- headers["X-Halite-Cached-Expires-At"] = @expires ? (cache_created_time(file) + @expires.not_nil!).to_s : "None"
- end
- end
- end
-
- return unless file
-
- if @debug
- headers["X-Halite-Cached-From"] = cache_from
- headers["X-Halite-Cached-At"] = cache_created_time(file).to_s
- end
-
- body = File.read_lines(file).join("\n")
- Response.new(request.uri, status_code, body, headers)
- end
-
- private def find_metadata(path)
- file = File.join(path, "metadata.json")
- if File.exists?(file)
- JSON.parse(File.open(file)).as_h
- end
- end
-
- private def cache_expired?(file)
- return false unless expires = @expires
- file_modified_time = cache_created_time(file)
- Time.utc >= (file_modified_time + expires)
- end
-
- private def cache_created_time(file)
- File.info(file).modification_time
- end
-
- private def generate_cache_key(request) : String
- Digest::MD5.hexdigest("#{request.verb}-#{request.uri}-#{request.body}")
- end
-
- private def write_cache(request, response)
- key = generate_cache_key(request)
- path = File.join(@path, key)
- FileUtils.mkdir_p(path) unless Dir.exists?(path)
-
- write_metadata(path, response)
- write_body(path, key, response)
- end
-
- private def write_metadata(path, response)
- File.open(File.join(path, "metadata.json"), "w") do |f|
- f.puts({
- "status_code" => response.status_code,
- "headers" => response.headers.to_flat_h,
- }.to_json)
- end
- end
-
- private def write_body(path, key, response)
- File.open(File.join(path, "#{key}.cache"), "w") do |f|
- f.puts response.body
- end
- end
-
- Halite.register_feature "cache", self
- end
-end
diff --git a/lib/halite/src/halite/features/logging.cr b/lib/halite/src/halite/features/logging.cr
deleted file mode 100644
index d4500868..00000000
--- a/lib/halite/src/halite/features/logging.cr
+++ /dev/null
@@ -1,120 +0,0 @@
-require "log"
-require "colorize"
-require "file_utils"
-
-Log.setup do |c|
- backend = Log::IOBackend.new(formatter: Halite::Logging::ShortFormat)
- c.bind("halite", :info, backend)
-end
-
-module Halite
- # Logging feature
- class Logging < Feature
- DEFAULT_LOGGER = Logging::Common.new
-
- getter writer : Logging::Abstract
-
- # return a new Cache instance
- #
- # Accepts argument:
- #
- # - **logging**: `Logging::Abstract`
- def initialize(**options)
- @writer = (logging = options[:logging]?) ? logging.as(Logging::Abstract) : DEFAULT_LOGGER
- end
-
- def request(request)
- @writer.request(request)
- request
- end
-
- def response(response)
- @writer.response(response)
- response
- end
-
- # Logging format Abstract
- abstract class Abstract
- setter logger : Log
- getter skip_request_body : Bool
- getter skip_response_body : Bool
- getter skip_benchmark : Bool
- getter colorize : Bool
-
- @request_time : Time?
-
- def initialize(*, for : String = "halite",
- @skip_request_body = false, @skip_response_body = false,
- @skip_benchmark = false, @colorize = true)
- @logger = Log.for(for)
- Colorize.enabled = @colorize
- end
-
- abstract def request(request)
- abstract def response(response)
-
- protected def human_time(elapsed : Time::Span)
- elapsed = elapsed.to_f
- case Math.log10(elapsed)
- when 0..Float64::MAX
- digits = elapsed
- suffix = "s"
- when -3..0
- digits = elapsed * 1000
- suffix = "ms"
- when -6..-3
- digits = elapsed * 1_000_000
- suffix = "ยตs"
- else
- digits = elapsed * 1_000_000_000
- suffix = "ns"
- end
-
- "#{digits.round(2).to_s}#{suffix}"
- end
- end
-
- @@formats = {} of String => Abstract.class
-
- # Logging format register
- module Register
- def register(name : String, format : Abstract.class)
- @@formats[name] = format
- end
-
- def [](name : String)
- @@formats[name]
- end
-
- def []?(name : String)
- @@formats[name]?
- end
-
- def availables
- @@formats.keys
- end
- end
-
- # Similar to `Log::ShortFormat`
- #
- # **NOTE**: It invalid by calling `Log.setup` or `Log.setup_from_env` outside of Halite.
- #
- # Copy from https://github.com/crystal-lang/crystal/blob/3c48f311f/src/log/format.cr#L197
- struct ShortFormat < Log::StaticFormatter
- def run
- "#{timestamp} - #{source(before: " ", after: ": ")}#{message}" \
- "#{data(before: " -- ")}#{context(before: " -- ")}#{exception}"
- end
-
- def timestamp
- Helper.to_rfc3339(@entry.timestamp, @io)
- end
- end
-
- extend Register
-
- Halite.register_feature "logging", self
- end
-end
-
-require "./logging/*"
diff --git a/lib/halite/src/halite/features/logging/common.cr b/lib/halite/src/halite/features/logging/common.cr
deleted file mode 100644
index 892f520f..00000000
--- a/lib/halite/src/halite/features/logging/common.cr
+++ /dev/null
@@ -1,112 +0,0 @@
-class Halite::Logging
- # Common logging format
- #
- # Instance variables to check `Halite::Logging::Abstract`
- #
- # ```
- # Halite.use("logging", logging: Halite::Logging::Common.new(skip_request_body: true))
- # .get("http://httpbin.org/get")
- #
- # # Or
- # Halite.logging(format: "common", skip_request_body: true)
- # .get("http://httpbin.org/get")
- #
- # # => 2018-08-31 16:56:12 +08:00 | request | GET | http://httpbin.org/get
- # # => 2018-08-31 16:56:13 +08:00 | response | 200 | http://httpbin.org/get | 1.08s | application/json
- # ```
- class Common < Abstract
- def request(request)
- message = String.build do |io|
- io << "> | request | " << colorful_method(request.verb)
- io << "| " << request.uri
- unless request.body.empty? || @skip_request_body
- io << "\n" << request.body
- end
- end
-
- @logger.info { message }
- @request_time = Time.utc unless @skip_benchmark
- end
-
- def response(response)
- message = String.build do |io|
- content_type = response.content_type || "Unknown MIME"
- io << "< | response | " << colorful_status_code(response.status_code)
- io << "| " << response.uri
- if !@skip_benchmark && (request_time = @request_time)
- elapsed = Time.utc - request_time
- io << " | " << human_time(elapsed)
- end
-
- io << " | " << content_type
- unless response.body.empty? || binary_type?(content_type) || @skip_response_body
- io << "\n" << response.body
- end
- end
-
- @logger.info { message }
- end
-
- protected def colorful_method(method, is_request = true)
- fore, back = case method.upcase
- when "GET"
- [:white, :blue]
- when "POST"
- [:white, :cyan]
- when "PUT"
- [:white, :yellow]
- when "DELETE"
- [:white, :red]
- when "PATCH"
- [:white, :green]
- when "HEAD"
- [:white, :magenta]
- else
- [:dark_gray, :white]
- end
-
- colorful((" %-7s" % method), fore, back)
- end
-
- protected def colorful_status_code(status_code : Int32)
- fore, back = case status_code
- when 300..399
- [:dark_gray, :white]
- when 400..499
- [:white, :yellow]
- when 500..999
- [:white, :red]
- else
- [:white, :green]
- end
-
- colorful((" %-7s" % status_code), fore, back)
- end
-
- protected def colorful(message, fore, back)
- Colorize.enabled = !!(@colorize && (backend = @logger.backend.as?(Log::IOBackend)) && backend.io.tty?)
-
- message.colorize.fore(fore).back(back)
- end
-
- # return `true` if is binary types with MIME type
- #
- # MIME types list: https://developer.mozilla.org/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Complete_list_of_MIME_types
- private def binary_type?(content_type)
- binary_types = %w(image audio video)
- application_types = %w(pdf octet-stream ogg 3gpp ebook archive rar zip tar 7z word powerpoint excel flash font)
-
- binary_types.each do |name|
- return true if content_type.starts_with?(name)
- end
-
- application_types.each do |name|
- return true if content_type.starts_with?("application") && content_type.includes?(name)
- end
-
- false
- end
-
- Logging.register "common", self
- end
-end
diff --git a/lib/halite/src/halite/features/logging/json.cr b/lib/halite/src/halite/features/logging/json.cr
deleted file mode 100644
index da980bc9..00000000
--- a/lib/halite/src/halite/features/logging/json.cr
+++ /dev/null
@@ -1,106 +0,0 @@
-require "json"
-
-class Halite::Logging
- # JSON logging format
- #
- # Instance variables to check `Halite::Logging::Abstract`.
- #
- # In JSON format, if you set skip some key, it will return `false`.
- #
- # ```
- # Halite.use("logging", logging: Halite::Logging::JSON.new(skip_request_body: true))
- # .get("http://httpbin.org/get")
- #
- # # Or
- # Halite.logging(format: "json", skip_request_body: true)
- # .get("http://httpbin.org/get")
- # ```
- #
- # Log will look like:
- #
- # ```
- # {
- # "created_at": "2018-08-31T16:53:57+08:00:00",
- # "entry": {
- # "request": {
- # "body": "",
- # "headers": {...},
- # "method": "GET",
- # "url": "http://httpbin.org/anything",
- # "timestamp": "2018-08-31T16:53:59+08:00:00",
- # },
- # "response": {
- # "body": false,
- # "header": {...},
- # "status_code": 200,
- # "http_version": "HTTP/1.1",
- # "timestamp": "2018-08-31T16:53:59+08:00:00",
- # },
- # },
- # }
- # ```
- class JSON < Abstract
- @request : Request? = nil
- @response : Response? = nil
-
- def request(request)
- @request_time = Time.utc
- @request = Request.new(request, @skip_request_body)
- end
-
- def response(response)
- @response = Response.new(response, @skip_response_body)
- @logger.info { raw }
- end
-
- private def raw
- elapsed : String? = nil
- if !@skip_benchmark && (request_time = @request_time)
- elapsed = human_time(Time.utc - request_time)
- end
-
- {
- "created_at" => Helper.to_rfc3339(@request_time.not_nil!),
- "elapsed" => elapsed,
- "entry" => {
- "request" => @request.not_nil!.to_h,
- "response" => @response.not_nil!.to_h,
- },
- }.to_pretty_json
- end
-
- # :nodoc:
- private struct Request
- def initialize(@request : Halite::Request, @skip_body = false)
- end
-
- def to_h
- {
- "body" => @skip_body ? false : @request.body,
- "headers" => @request.headers.to_flat_h,
- "method" => @request.verb,
- "url" => @request.uri.to_s,
- "timestamp" => Helper.to_rfc3339(Time.utc),
- }
- end
- end
-
- # :nodoc:
- private struct Response
- def initialize(@response : Halite::Response, @skip_body = false)
- end
-
- def to_h
- {
- "body" => @skip_body ? false : @response.body,
- "header" => @response.headers.to_flat_h,
- "status_code" => @response.status_code,
- "http_version" => @response.version,
- "timestamp" => Helper.to_rfc3339(Time.utc),
- }
- end
- end
-
- Logging.register "json", self
- end
-end
diff --git a/lib/halite/src/halite/form_data.cr b/lib/halite/src/halite/form_data.cr
deleted file mode 100644
index 707d53b3..00000000
--- a/lib/halite/src/halite/form_data.cr
+++ /dev/null
@@ -1,68 +0,0 @@
-require "http/formdata"
-require "mime/multipart"
-
-module Halite
- # Utility-belt to build form data request bodies.
- #
- # Provides support for `application/x-www-form-urlencoded` and
- # `multipart/form-data` types.
- #
- # ```
- # form = FormData.create({
- # "name" => "Lizeth Gusikowski",
- # "skill" => ["ruby", "crystal"],
- # "avatar" => File.open("avatar.png"), # => "image binary data"
- # })
- #
- # form.body # => "----------------------------_ytTht-0D5oif0cAGXSPjPSN\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\nLizeth Gusikowski\r\n----------------------------_ytTht-0D5oif0cAGXSPjPSN\r\nContent-Disposition: form-data; name=\"skill\"\r\n\r\nruby\r\n----------------------------_ytTht-0D5oif0cAGXSPjPSN\r\nContent-Disposition: form-data; name=\"skill\"\r\n\r\ncrystal\r\n----------------------------_ytTht-0D5oif0cAGXSPjPSN\r\nContent-Disposition: form-data; name=\"avatar\"; filename=\"avatar.png\"\r\n\r\nimage binary data\n\r\n----------------------------_ytTht-0D5oif0cAGXSPjPSN--"
- # form.headers # => HTTP::Headers{"Content-Type" => "multipart/form-data; boundary=\"--------------------------SS0a9QKeM_6fcj2CE5D4d0LQ\""}
- # ```
- module FormData
- # FormData factory. Automatically selects best type depending on given `data` Hash
- def self.create(data : Hash(String, Halite::Options::Type) = {} of String => Halite::Options::Type) : Halite::Request::Data
- if multipart?(data)
- io = IO::Memory.new
- builder = HTTP::FormData::Builder.new(io)
- data.each do |k, v|
- case v
- when File
- builder.file(k, v.as(IO), HTTP::FormData::FileMetadata.new(filename: File.basename(v.path)))
- when Array
- v.each do |e|
- case e
- when File
- builder.file(k, e.as(IO), HTTP::FormData::FileMetadata.new(filename: File.basename(e.path)))
- else
- builder.field(k, e.to_s)
- end
- end
- else
- builder.field(k, v.to_s)
- end
- end
- builder.finish
-
- Halite::Request::Data.new(io.to_s, builder.content_type)
- else
- body = HTTP::Params.encode(data)
- Halite::Request::Data.new(body, "application/x-www-form-urlencoded")
- end
- end
-
- # Tells whenever data contains multipart data or not.
- private def self.multipart?(data : Hash(String, Halite::Options::Type)) : Bool
- data.any? do |_, v|
- case v
- when File
- next true
- when Array
- v.any? do |vv|
- next true if vv.is_a?(File)
- end
- else
- false
- end
- end
- end
- end
-end
diff --git a/lib/halite/src/halite/header_link.cr b/lib/halite/src/halite/header_link.cr
deleted file mode 100644
index 25b6bbf9..00000000
--- a/lib/halite/src/halite/header_link.cr
+++ /dev/null
@@ -1,59 +0,0 @@
-module Halite
- # HeaderLink
- #
- # ref: [https://tools.ietf.org/html/rfc5988](https://tools.ietf.org/html/rfc5988)
- struct HeaderLink
- # Header link parser
- def self.parse(raw : String, uri : URI? = nil) : Hash(String, Halite::HeaderLink)
- links = {} of String => HeaderLink
- raw.split(/,\s*).each do |rel|
- head_link = parse_link(rel, uri)
- links[head_link.rel] = head_link
- end
- links
- end
-
- private def self.parse_link(raw, uri)
- params = {} of String => String
- if raw.includes?(";")
- target, attrs = raw.split(";", 2)
- rel = target = target.gsub(/[<> '\"]/, "").strip
- unless attrs.strip.empty?
- attrs.split(";").each do |attr|
- next if attr.strip.empty?
- key, value = attr.split("=")
- key = key.gsub(/['\"]/, "").strip
- next if params.has_key?(key)
-
- value = value.gsub(/['\"]/, "").strip
- params[key] = value
- end
-
- if name = params.delete("rel")
- rel = name
- if target == "/"
- target = rel
- elsif target.starts_with?("/") && (uri_local = uri)
- full_target = uri_local.dup
- full_target.path = target
- target = full_target.to_s
- end
- end
- end
- else
- rel = target = raw.gsub(/[<> '\"]/, "").strip
- end
-
- new(rel, target, params)
- end
-
- getter rel, target, params
-
- def initialize(@rel : String, @target : String, @params : Hash(String, String))
- end
-
- def to_s(io)
- io << target
- end
- end
-end
diff --git a/lib/halite/src/halite/mime_type.cr b/lib/halite/src/halite/mime_type.cr
deleted file mode 100644
index ca871e6f..00000000
--- a/lib/halite/src/halite/mime_type.cr
+++ /dev/null
@@ -1,33 +0,0 @@
-module Halite
- module MimeType
- @@adapters = {} of String => MimeType::Adapter
- @@aliases = {} of String => String
-
- def self.register(adapter : MimeType::Adapter, name : String, *shortcuts)
- @@adapters[name] = adapter
- shortcuts.each do |shortcut|
- next unless shortcut.is_a?(String)
- @@aliases[shortcut] = name
- end unless shortcuts.empty?
- end
-
- def self.[](name : String)
- @@adapters[normalize name]
- end
-
- def self.[]?(name : String)
- @@adapters[normalize name]?
- end
-
- private def self.normalize(name : String)
- @@aliases.fetch name, name
- end
-
- abstract class Adapter
- abstract def encode(obj)
- abstract def decode(string)
- end
- end
-end
-
-require "./mime_types/*"
diff --git a/lib/halite/src/halite/mime_types/json.cr b/lib/halite/src/halite/mime_types/json.cr
deleted file mode 100644
index 65191909..00000000
--- a/lib/halite/src/halite/mime_types/json.cr
+++ /dev/null
@@ -1,15 +0,0 @@
-require "json"
-
-module Halite::MimeType
- class JSON < Adapter
- def encode(obj)
- obj.to_json
- end
-
- def decode(str)
- ::JSON.parse str
- end
- end
-end
-
-Halite::MimeType.register Halite::MimeType::JSON.new, "application/json", "json"
diff --git a/lib/halite/src/halite/options.cr b/lib/halite/src/halite/options.cr
deleted file mode 100644
index b70f12df..00000000
--- a/lib/halite/src/halite/options.cr
+++ /dev/null
@@ -1,495 +0,0 @@
-require "openssl"
-require "./options/*"
-
-module Halite
- # Options class
- #
- # ### Init with splats options
- #
- # ```
- # o = Options.new(
- # headers: {
- # user_agent: "foobar"
- # }
- # }
- # o.headers.class # => HTTP::Headers
- # o.cookies.class # => HTTP::Cookies
- # ```
- #
- # ### Set/Get timeout
- #
- # Set it with `connect_timeout`/`read_timeout`/`write_timeout` keys,
- # but get it call `Timeout` class.
- #
- # ```
- # o = Options.new(connect_timeout: 30, read_timeout: 30)
- # o.timeout.connect # => 30.0
- # o.timeout.read # => 30.0
- # o.timeout.write # => nil
- # ```
- #
- # ### Set/Get follow
- #
- # Set it with `follow`/`follow_strict` keys, but get it call `Follow` class.
- #
- # ```
- # o = Options.new(follow: 3, follow_strict: false)
- # o.follow.hops # => 3
- # o.follow.strict # => false
- # ```
- class Options
- def self.new(endpoint : (String | URI)? = nil,
- headers : (Hash(String, _) | NamedTuple)? = nil,
- cookies : (Hash(String, _) | NamedTuple)? = nil,
- params : (Hash(String, _) | NamedTuple)? = nil,
- form : (Hash(String, _) | NamedTuple)? = nil,
- json : (Hash(String, _) | NamedTuple)? = nil,
- raw : String? = nil,
- connect_timeout : (Int32 | Float64 | Time::Span)? = nil,
- read_timeout : (Int32 | Float64 | Time::Span)? = nil,
- write_timeout : (Int32 | Float64 | Time::Span)? = nil,
- follow : Int32? = nil,
- follow_strict : Bool? = nil,
- tls : OpenSSL::SSL::Context::Client? = nil,
- features = {} of String => Feature)
- new(
- endpoint: endpoint,
- headers: headers,
- cookies: cookies,
- params: params,
- form: form,
- json: json,
- raw: raw,
- timeout: Timeout.new(connect: connect_timeout, read: read_timeout, write: write_timeout),
- follow: Follow.new(hops: follow, strict: follow_strict),
- tls: tls,
- features: features
- )
- end
-
- # Types of options in a Hash
- alias Type = Nil | Symbol | String | Int32 | Int64 | Float64 | Bool | File | Array(Type) | Hash(String, Type)
-
- property endpoint : URI?
- property headers : HTTP::Headers
- property cookies : HTTP::Cookies
- property timeout : Timeout
- property follow : Follow
- property tls : OpenSSL::SSL::Context::Client?
-
- property params : Hash(String, Type)
- property form : Hash(String, Type)
- property json : Hash(String, Type)
- property raw : String?
-
- property features : Hash(String, Feature)
-
- def initialize(*,
- endpoint : (String | URI)? = nil,
- headers : (Hash(String, _) | NamedTuple)? = nil,
- cookies : (Hash(String, _) | NamedTuple)? = nil,
- params : (Hash(String, _) | NamedTuple)? = nil,
- form : (Hash(String, _) | NamedTuple)? = nil,
- json : (Hash(String, _) | NamedTuple)? = nil,
- @raw : String? = nil,
- @timeout = Timeout.new,
- @follow = Follow.new,
- @tls : OpenSSL::SSL::Context::Client? = nil,
- @features = {} of String => Feature)
- @endpoint = parse_endpoint(endpoint)
- @headers = parse_headers(headers)
- @cookies = parse_cookies(cookies)
- @params = parse_params(params)
- @form = parse_form(form)
- @json = parse_json(json)
- end
-
- def initialize(*,
- @endpoint : URI?,
- @headers : HTTP::Headers,
- @cookies : HTTP::Cookies,
- @params : Hash(String, Type),
- @form : Hash(String, Type),
- @json : Hash(String, Type),
- @raw : String? = nil,
- @timeout = Timeout.new,
- @follow = Follow.new,
- @tls : OpenSSL::SSL::Context::Client? = nil,
- @features = {} of String => Feature)
- end
-
- def with_endpoint(endpoint : String | URI)
- self.endpoint = endpoint
- self
- end
-
- # Alias `with_headers` method.
- def with_headers(**with_headers) : Halite::Options
- with_headers(with_headers)
- end
-
- # Returns `Options` self with given headers combined.
- def with_headers(headers : Hash(String, _) | NamedTuple) : Halite::Options
- @headers.merge!(parse_headers(headers))
- self
- end
-
- # Alias `with_cookies` method.
- def with_cookies(**cookies) : Halite::Options
- with_cookies(cookies)
- end
-
- # Returns `Options` self with given cookies combined.
- def with_cookies(cookies : Hash(String, _) | NamedTuple) : Halite::Options
- cookies.each do |key, value|
- @cookies[key.to_s] = value.to_s
- end
-
- self
- end
-
- # Returns `Options` self with given cookies combined.
- def with_cookies(cookies : HTTP::Cookies) : Halite::Options
- cookies.each do |cookie|
- with_cookies(cookie)
- end
-
- self
- end
-
- # Returns `Options` self with given cookies combined.
- def with_cookies(cookie : HTTP::Cookie) : Halite::Options
- cookie_header = HTTP::Headers{"Cookie" => cookie.to_cookie_header}
- @headers.merge!(cookie_header)
- @cookies.fill_from_client_headers(@headers)
- self
- end
-
- # Returns `Options` self with given max hops of redirect times.
- #
- # ```
- # # Automatically following redirects
- # options.with_follow
- # # A maximum of 3 subsequent redirects
- # options.with_follow(3)
- # # Set subsequent redirects
- # options.with_follow(3)
- # ```
- def with_follow(follow = Follow::MAX_HOPS, strict = Follow::STRICT) : Halite::Options
- @follow.hops = follow
- @follow.strict = strict
- self
- end
-
- # Returns `Options` self with given connect, read timeout.
- def with_timeout(connect : (Int32 | Float64 | Time::Span)? = nil,
- read : (Int32 | Float64 | Time::Span)? = nil,
- write : (Int32 | Float64 | Time::Span)? = nil) : Halite::Options
- @timeout.connect = connect.to_f if connect
- @timeout.read = read.to_f if read
- @timeout.write = write.to_f if write
-
- self
- end
-
- # Returns `Options` self with the name of features.
- def with_features(*features)
- features.each do |feature|
- with_features(feature, NamedTuple.new)
- end
- self
- end
-
- # Returns `Options` self with feature name and options.
- def with_features(feature_name : String, **opts)
- with_features(feature_name, opts)
- end
-
- # Returns `Options` self with feature name and options.
- def with_features(name : String, opts : NamedTuple)
- raise UnRegisterFeatureError.new("Not available feature: #{name}") unless klass = Halite.feature?(name)
- @features[name] = klass.new(**opts)
- self
- end
-
- # Returns `Options` self with feature name and feature.
- def with_features(name : String, feature : Feature)
- @features[name] = feature
- self
- end
-
- # Returns `Options` iitself with given format and the options of format.
- def with_logging(format : String, **opts)
- raise UnRegisterLoggerFormatError.new("Not available logging format: #{format}") unless format_cls = Logging[format]?
- with_logging(format_cls.new(**opts))
- end
-
- # Returns `Options` self with given logging, depend on `with_features`.
- def with_logging(logging : Halite::Logging::Abstract)
- with_features("logging", logging: logging)
- self
- end
-
- # Set endpoint of request
- def endpoint=(endpoint : String)
- @endpoint = URI.parse(endpoint)
- end
-
- # Set headers of request
- def headers=(headers : (Hash(String, _) | NamedTuple))
- @headers = parse_headers(headers)
- end
-
- # Alias `Timeout.connect`
- def connect_timeout
- @timeout.connect
- end
-
- # Alias `Timeout.connect=`
- def connect_timeout=(timeout : Int32 | Float64 | Time::Span)
- @timeout.connect = timeout
- end
-
- # Alias `Timeout.read`
- def read_timeout
- @timeout.read
- end
-
- # Alias `Timeout.read=`
- def read_timeout=(timeout : Int32 | Float64 | Time::Span)
- @timeout.read = timeout
- end
-
- # Alias `Timeout.write`
- def write_timeout
- @timeout.write
- end
-
- # Alias `Timeout.write=`
- def write_timeout=(timeout : Int32 | Float64 | Time::Span)
- @timeout.write = timeout
- end
-
- # Alias `Follow.hops=`
- def follow=(hops : Int32)
- @follow.hops = hops
- end
-
- # Alias `Follow.strict`
- def follow_strict
- @follow.strict
- end
-
- # Alias `Follow.strict=`
- def follow_strict=(strict : Bool)
- @follow.strict = strict
- end
-
- # Get logging status
- def logging : Bool
- !@features.values.select { |v| v.is_a?(Halite::Logging) }.empty?
- end
-
- # Quick enable logging
- #
- # By defaults, use `Logging::Common` as logging output.
- def logging=(enable : Bool)
- if enable
- with_features("logging") unless logging
- else
- @features.delete("logging")
- end
- end
-
- # Merge with other `Options` and return new `Halite::Options`
- def merge(other : Halite::Options) : Halite::Options
- options = Halite::Options.new
- options.merge!(dup)
- options.merge!(other)
- options
- end
-
- # Merge with other `Options` and return self
- def merge!(other : Halite::Options) : Halite::Options
- @endpoint = other.endpoint if other.endpoint
-
- @headers.merge!(other.headers)
-
- other.cookies.each do |cookie|
- @cookies << cookie
- end if other.cookies != @cookies
-
- if other.timeout.connect || other.timeout.read || other.timeout.write
- @timeout = other.timeout
- end
-
- if other.follow.updated?
- @follow = other.follow
- end
-
- @features.merge!(other.features) unless other.features.empty?
- @params.merge!(other.params) if other.params
- @form.merge!(other.form) if other.form
- @json.merge!(other.json) if other.json
- @raw = other.raw if other.raw
- @tls = other.tls if other.tls
-
- self
- end
-
- # Reset options
- def clear! : Halite::Options
- @endpoint = nil
- @headers = HTTP::Headers.new
- @cookies = HTTP::Cookies.new
- @params = {} of String => Type
- @form = {} of String => Type
- @json = {} of String => Type
- @raw = nil
- @timeout = Timeout.new
- @follow = Follow.new
- @features = {} of String => Feature
- @tls = nil
-
- self
- end
-
- # Produces a shallow copy of objโthe instance variables of obj are copied,
- # but not the objects they reference. dup copies the tainted state of obj.
- def dup
- Halite::Options.new(
- endpoint: @endpoint,
- headers: @headers.dup,
- cookies: @cookies,
- params: @params,
- form: @form,
- json: @json,
- raw: @raw,
- timeout: @timeout,
- follow: @follow,
- features: @features,
- tls: @tls
- )
- end
-
- # Returns this collection as a plain Hash.
- def to_h
- {
- "endpoint" => @endpoint,
- "headers" => @headers.to_h,
- "cookies" => @cookies.to_h,
- "params" => @params ? @params.to_h : nil,
- "form" => @form ? @form.to_h : nil,
- "json" => @json ? @json.to_h : nil,
- "raw" => @raw,
- "connect_timeout" => @timeout.connect,
- "read_timeout" => @timeout.read,
- "follow" => @follow.hops,
- "follow_strict" => @follow.strict,
- }
- end
-
- private def parse_endpoint(endpoint : (String | URI)?) : URI?
- case endpoint
- when String
- URI.parse(endpoint)
- when URI
- endpoint.as(URI)
- else
- nil
- end
- end
-
- private def parse_headers(raw : (Hash(String, _) | NamedTuple | HTTP::Headers)?) : HTTP::Headers
- case raw
- when Hash, NamedTuple
- HTTP::Headers.encode(raw)
- when HTTP::Headers
- raw.as(HTTP::Headers)
- else
- HTTP::Headers.new
- end
- end
-
- private def parse_cookies(raw : (Hash(String, _) | NamedTuple | HTTP::Cookies)?) : HTTP::Cookies
- cookies = HTTP::Cookies.from_client_headers(@headers)
- if objects = raw
- objects.each do |key, value|
- cookies[key] = case value
- when HTTP::Cookie
- value
- else
- value.to_s
- end
- end
- end
- cookies
- end
-
- private def parse_cookies(headers : HTTP::Headers) : HTTP::Cookies
- cookies = HTTP::Cookies.from_client_headers(headers)
- end
-
- {% for attr in %w(params form json) %}
- private def parse_{{ attr.id }}(raw : (Hash(String, _) | NamedTuple)?) : Hash(String, Options::Type)
- new_{{ attr.id }} = {} of String => Type
- return new_{{ attr.id }} unless {{ attr.id }} = raw
-
- if {{ attr.id }}.responds_to?(:each)
- {{ attr.id }}.each do |key, value|
- new_{{ attr.id }}[key.to_s] = case value
- when Array
- cast_hash(value.as(Array))
- when Hash
- cast_hash(value.as(Hash))
- when NamedTuple
- cast_hash(value.as(NamedTuple))
- when Type
- value
- else
- value.as(Type)
- end
- end
- end
-
- new_{{ attr.id }}
- end
- {% end %}
-
- private def cast_hash(raw : Array) : Options::Type
- raw.each_with_object([] of Type) do |value, obj|
- obj << case value
- when Array
- cast_hash(value.as(Array))
- when Hash
- cast_hash(value.as(Hash))
- when NamedTuple
- cast_hash(value.as(NamedTuple))
- else
- value.as(Type)
- end
- end.as(Type)
- end
-
- private def cast_hash(raw : Hash) : Options::Type
- raw.each_with_object({} of String => Type) do |(key, value), obj|
- if key.responds_to?(:to_s)
- obj[key.to_s] = case value
- when Array
- cast_hash(value.as(Array))
- when Hash
- cast_hash(value.as(Hash))
- when NamedTuple
- cast_hash(value.as(NamedTuple))
- else
- value.as(Type)
- end
- end
- end.as(Type)
- end
-
- private def cast_hash(raw : NamedTuple) : Options::Type
- cast_hash(raw.to_h)
- end
- end
-end
diff --git a/lib/halite/src/halite/options/follow.cr b/lib/halite/src/halite/options/follow.cr
deleted file mode 100644
index 9ff8264c..00000000
--- a/lib/halite/src/halite/options/follow.cr
+++ /dev/null
@@ -1,33 +0,0 @@
-module Halite
- class Options
- struct Follow
- # No follow by default
- DEFAULT_HOPS = 0
-
- # A maximum of 5 subsequent redirects
- MAX_HOPS = 5
-
- # Redirector hops policy
- STRICT = true
-
- property hops : Int32
- property strict : Bool
-
- def initialize(hops : Int32? = nil, strict : Bool? = nil)
- @hops = hops || DEFAULT_HOPS
- @strict = strict.nil? ? STRICT : strict
- end
-
- def strict?
- @strict == true
- end
-
- def updated?
- @hops != DEFAULT_HOPS || @strict != STRICT
- end
- end
- end
-
- # :nodoc:
- alias Follow = Options::Follow
-end
diff --git a/lib/halite/src/halite/options/timeout.cr b/lib/halite/src/halite/options/timeout.cr
deleted file mode 100644
index 9c3b043c..00000000
--- a/lib/halite/src/halite/options/timeout.cr
+++ /dev/null
@@ -1,46 +0,0 @@
-module Halite
- class Options
- # Timeout struct
- struct Timeout
- getter connect : Float64?
- getter read : Float64?
- getter write : Float64?
-
- def initialize(connect : (Int32 | Float64 | Time::Span)? = nil,
- read : (Int32 | Float64 | Time::Span)? = nil,
- write : (Int32 | Float64 | Time::Span)? = nil)
- @connect = timeout_value(connect)
- @read = timeout_value(read)
- @write = timeout_value(write)
- end
-
- def connect=(connect : (Int32 | Float64 | Time::Span)?)
- @connect = timeout_value(connect)
- end
-
- def read=(read : (Int32 | Float64 | Time::Span)?)
- @read = timeout_value(read)
- end
-
- def write=(write : (Int32 | Float64 | Time::Span)?)
- @write = timeout_value(write)
- end
-
- private def timeout_value(value : (Int32 | Float64 | Time::Span)? = nil) : Float64?
- case value
- when Int32
- value.as(Int32).to_f
- when Float64
- value.as(Float64)
- when Time::Span
- value.as(Time::Span).total_seconds.to_f
- else
- nil
- end
- end
- end
- end
-
- # :nodoc:
- alias Timeout = Options::Timeout
-end
diff --git a/lib/halite/src/halite/rate_limit.cr b/lib/halite/src/halite/rate_limit.cr
deleted file mode 100644
index 1f05e374..00000000
--- a/lib/halite/src/halite/rate_limit.cr
+++ /dev/null
@@ -1,30 +0,0 @@
-module Halite
- # Limit Rate
- #
- # ref: [https://tools.ietf.org/id/draft-polli-ratelimit-headers-00.html](https://tools.ietf.org/id/draft-polli-ratelimit-headers-00.html)
- #
- # ```
- # > X-RateLimit-Limit: 5000
- # > X-RateLimit-Remaining: 4987
- # > X-RateLimit-Reset: 1350085394
- # ```
- struct RateLimit
- RATELIMIT_LIMIT = "X-RateLimit-Limit"
- RATELIMIT_REMAINING = "X-RateLimit-Remaining"
- RATELIMIT_RESET = "X-RateLimit-Reset"
-
- def self.parse(headers : HTTP::Headers)
- limit = headers[RATELIMIT_LIMIT]?.try &.to_i
- remaining = headers[RATELIMIT_REMAINING]?.try &.to_i
- reset = headers[RATELIMIT_RESET]?.try &.to_i
- return if !limit && !remaining && !reset
-
- new(limit, remaining, reset)
- end
-
- getter limit, remaining, reset
-
- def initialize(@limit : Int32?, @remaining : Int32?, @reset : Int32?)
- end
- end
-end
diff --git a/lib/halite/src/halite/redirector.cr b/lib/halite/src/halite/redirector.cr
deleted file mode 100644
index d639fe38..00000000
--- a/lib/halite/src/halite/redirector.cr
+++ /dev/null
@@ -1,84 +0,0 @@
-module Halite
- class Redirector
- # HTTP status codes which indicate redirects
- REDIRECT_CODES = [300, 301, 302, 303, 307, 308]
-
- # Codes which which should raise StateError in strict mode if original
- # request was any of {UNSAFE_VERBS}
- STRICT_SENSITIVE_CODES = [300, 301, 302]
-
- # Insecure http verbs, which should trigger StateError in strict mode
- # upon {STRICT_SENSITIVE_CODES}
- UNSAFE_VERBS = %w(PUT DELETE POST)
-
- # Verbs which will remain unchanged upon See Other response.
- SEE_OTHER_ALLOWED_VERBS = %w(GET HEAD)
-
- def self.new(request : Halite::Request, response : Halite::Response, options : Halite::Options)
- new(request, response, options.follow.hops, options.follow.strict)
- end
-
- getter strict : Bool
- getter max_hops : Int32
-
- # Instance a new Redirector
- def initialize(@request : Halite::Request, @response : Halite::Response, @max_hops = 5, @strict = true)
- @visited = [] of String
- end
-
- # Follows redirects until non-redirect response found
- def perform(&block : Halite::Request -> Halite::Response) : Halite::Response
- if avaiable?
- each_redirect do |request|
- block.call(request)
- end
- end
-
- @response
- end
-
- # Loop each redirect request with block call
- def each_redirect(&block : Halite::Request -> Halite::Response)
- while avaiable?
- @visited << "#{@request.verb} #{@request.uri}"
-
- raise TooManyRedirectsError.new if too_many_hops?
- raise EndlessRedirectError.new if endless_loop?
-
- @request = redirect_to(@response.headers["Location"]?)
- @response = block.call(@request)
- end
- end
-
- # Return `true` if it should redirect, else `false`
- def avaiable?
- REDIRECT_CODES.includes?(@response.status_code)
- end
-
- # Redirect policy for follow
- private def redirect_to(uri : String?)
- raise StateError.new("No found `Location` in headers") unless uri
-
- verb = @request.verb
- code = @response.status_code
-
- if UNSAFE_VERBS.includes?(verb) && STRICT_SENSITIVE_CODES.includes?(code)
- raise StateError.new("Can not follow #{code} redirect") if @strict
- verb = "GET"
- end
-
- verb = "GET" if !SEE_OTHER_ALLOWED_VERBS.includes?(verb) && code == 303
- @request.redirect(uri, verb)
- end
-
- # Check if we reached max amount of redirect hops
- private def too_many_hops? : Bool
- 1 <= @max_hops && @max_hops < @visited.size
- end
-
- # Check if we got into an endless loop
- def endless_loop?
- 2 <= @visited.count(@visited.last)
- end
- end
-end
diff --git a/lib/halite/src/halite/request.cr b/lib/halite/src/halite/request.cr
deleted file mode 100644
index 637b5362..00000000
--- a/lib/halite/src/halite/request.cr
+++ /dev/null
@@ -1,92 +0,0 @@
-module Halite
- class Request
- # Allowed methods
- #
- # See more: [https://github.com/crystal-lang/crystal/blob/863f301cfe9e9757a6bf1a494ab7bf49bfc07a06/src/http/client.cr#L329](https://github.com/crystal-lang/crystal/blob/863f301cfe9e9757a6bf1a494ab7bf49bfc07a06/src/http/client.cr#L329)
- METHODS = %w(GET PUT POST DELETE HEAD PATCH OPTIONS)
-
- # Allowed schemes
- SCHEMES = %w(http https)
-
- # Request user-agent by default
- USER_AGENT = "Halite/#{Halite::VERSION}"
-
- # The verb name of request
- getter verb : String
-
- # The uri of request
- getter uri : URI
-
- # The scheme name of request
- getter scheme : String
-
- # The headers of request
- getter headers : HTTP::Headers
-
- # The payload of request
- getter body : String
-
- def initialize(verb : String, @uri : URI, @headers : HTTP::Headers = HTTP::Headers.new, @body : String = "")
- @verb = verb.upcase
-
- raise UnsupportedMethodError.new("Unknown method: #{@verb}") unless METHODS.includes?(@verb)
- raise UnsupportedSchemeError.new("Missing scheme: #{@uri}") unless @uri.scheme
-
- @scheme = @uri.scheme.not_nil!
-
- raise UnsupportedSchemeError.new("Unknown scheme: #{@scheme}") unless SCHEMES.includes?(@scheme)
-
- @headers["User-Agent"] ||= USER_AGENT
- @headers["Connection"] ||= "close"
- end
-
- # Returns new Request with updated uri
- def redirect(uri : String, verb = @verb)
- headers = @headers.dup
- headers.delete("Host")
-
- Request.new(verb, redirect_uri(domain, uri), headers, body)
- end
-
- # @return `URI` with the scheme, user, password, port and host combined
- def domain
- URI.new(@uri.scheme, @uri.host, @uri.port, "", nil, @uri.user, @uri.password, nil)
- end
-
- # @return `String` with the path, query and fragment combined
- def full_path
- String.build do |str|
- {% if Crystal::VERSION < "0.36.0" %}
- str << @uri.full_path
- {% else %}
- str << @uri.request_target
- {% end %}
- if @uri.fragment
- str << "#" << @uri.fragment
- end
- end
- end
-
- private def redirect_uri(source : URI, uri : String) : URI
- return source if uri == '/'
-
- new_uri = URI.parse(uri)
- # return a new uri with source and relative path
- unless new_uri.scheme && new_uri.host
- new_uri = source.dup.tap do |u|
- u.path = (uri[0] == '/') ? uri : "/#{uri}"
- end
- end
-
- new_uri
- end
-
- # Request data of body
- struct Data
- getter body, content_type
-
- def initialize(@body : String, @content_type : String? = nil)
- end
- end
- end
-end
diff --git a/lib/halite/src/halite/response.cr b/lib/halite/src/halite/response.cr
deleted file mode 100644
index 4048d014..00000000
--- a/lib/halite/src/halite/response.cr
+++ /dev/null
@@ -1,123 +0,0 @@
-module Halite
- class Response
- def self.new(uri : URI, status_code : Int32, body : String? = nil, headers = HTTP::Headers.new,
- status_message = nil, body_io : IO? = nil, version = "HTTP/1.1", history = [] of Halite::Response)
- conn = HTTP::Client::Response.new(status_code, body, headers, status_message, version, body_io)
- new(uri, conn, history)
- end
-
- getter uri
- getter conn
- getter history : Array(Response)
-
- def initialize(@uri : URI, @conn : HTTP::Client::Response, @history = [] of Halite::Response)
- end
-
- delegate version, to: @conn
- delegate status_code, to: @conn
- delegate status_message, to: @conn
- delegate content_type, to: @conn
- delegate success?, to: @conn
-
- delegate headers, to: @conn
- delegate charset, to: @conn
-
- delegate body, to: @conn
- delegate body_io, to: @conn
-
- # Content Length
- def content_length : Int64?
- if value = @conn.headers["Content-Length"]?
- value.to_i64
- end
- end
-
- # Return a `HTTP::Cookies` of parsed cookie headers or else nil.
- def cookies : HTTP::Cookies?
- cookies = @conn.cookies ? @conn.cookies : HTTP::Cookies.from_server_headers(@conn.headers)
-
- # Try to fix empty domain
- cookies.map do |cookie|
- cookie.domain = @uri.host unless cookie.domain
- cookie
- end
-
- cookies
- end
-
- # Return a list of parsed link headers proxies or else nil.
- def links : Hash(String, Halite::HeaderLink)?
- return unless raw = headers["Link"]?
-
- HeaderLink.parse(raw, uri)
- end
-
- def rate_limit : Halite::RateLimit?
- RateLimit.parse(headers)
- end
-
- # Raise `Halite::ClientError`/`Halite::ServerError` if one occurred.
- #
- # - `4XX` raise an `Halite::ClientError` exception
- # - `5XX` raise an `Halite::ServerError` exception
- # - return `nil` with other status code
- #
- # ```
- # Halite.get("https://httpbin.org/status/404").raise_for_status
- # # => Unhandled exception: 404 not found error with url: https://httpbin.org/status/404 (Halite::ClientError)
- #
- # Halite.get("https://httpbin.org/status/500", params: {"foo" => "bar"}).raise_for_status
- # # => Unhandled exception: 500 internal server error error with url: https://httpbin.org/status/500?foo=bar (Halite::ServerError)
- #
- # Halite.get("https://httpbin.org/status/301").raise_for_status
- # # => nil
- # ```
- def raise_for_status
- if status_code >= 400 && status_code < 500
- raise Halite::ClientError.new(status_code: status_code, uri: uri)
- elsif status_code >= 500 && status_code < 600
- raise Halite::ServerError.new(status_code: status_code, uri: uri)
- end
- end
-
- # Parse response body with corresponding MIME type adapter.
- def parse(name : String? = nil)
- name ||= content_type
- raise Halite::Error.new("Missing media type") unless name
- raise Halite::UnRegisterMimeTypeError.new("unregister MIME type adapter: #{name}") unless MimeType[name]?
- MimeType[name].decode to_s
- end
-
- # Return filename if it exists, else `Nil`.
- def filename : String?
- headers["Content-Disposition"]?.try do |value|
- value.split("filename=")[1]
- end
- end
-
- # Return raw of response
- def to_raw
- io = IO::Memory.new
- @conn.to_io(io)
- io
- end
-
- # Return status_code, headers and body in a array
- def to_a
- [@conn.status_code, @conn.headers, to_s]
- end
-
- # Return String eagerly consume the entire body as a string
- def to_s
- @conn.body? ? @conn.body : @conn.body_io.to_s
- end
-
- def inspect
- "#<#{self.class} #{version} #{status_code} #{status_message} #{headers.to_flat_h}>"
- end
-
- def to_s(io)
- io << to_s
- end
- end
-end
diff --git a/lib/json_mapping/.editorconfig b/lib/json_mapping/.editorconfig
deleted file mode 100644
index 163eb75c..00000000
--- a/lib/json_mapping/.editorconfig
+++ /dev/null
@@ -1,9 +0,0 @@
-root = true
-
-[*.cr]
-charset = utf-8
-end_of_line = lf
-insert_final_newline = true
-indent_style = space
-indent_size = 2
-trim_trailing_whitespace = true
diff --git a/lib/json_mapping/.gitignore b/lib/json_mapping/.gitignore
deleted file mode 100644
index 0bbd4a9f..00000000
--- a/lib/json_mapping/.gitignore
+++ /dev/null
@@ -1,9 +0,0 @@
-/docs/
-/lib/
-/bin/
-/.shards/
-*.dwarf
-
-# Libraries don't need dependency lock
-# Dependencies will be locked in applications that use them
-/shard.lock
diff --git a/lib/json_mapping/.travis.yml b/lib/json_mapping/.travis.yml
deleted file mode 100644
index 227f158d..00000000
--- a/lib/json_mapping/.travis.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-language: crystal
-
-script:
- - crystal spec
- - crystal tool format --check
diff --git a/lib/json_mapping/LICENSE b/lib/json_mapping/LICENSE
deleted file mode 100644
index edcccc57..00000000
--- a/lib/json_mapping/LICENSE
+++ /dev/null
@@ -1,21 +0,0 @@
-The MIT License (MIT)
-
-Copyright (c) 2020 Manas Technology Solutions
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
diff --git a/lib/json_mapping/README.md b/lib/json_mapping/README.md
deleted file mode 100644
index abef1406..00000000
--- a/lib/json_mapping/README.md
+++ /dev/null
@@ -1,56 +0,0 @@
-# json_mapping
-
-Provides the legacy `JSON.mapping` macro method.
-
-This shard is provided as-is and considered deprecated. It won't receive feature enhancements.
-
-Please consider using [`JSON::Serializable`](https://crystal-lang.org/api/latest/JSON/Serializable.html) instead, the successor included in Crystal's standard library.
-
-## Installation
-
-1. Add the dependency to your `shard.yml`:
-
-```yaml
-dependencies:
- json_mapping:
- github: crystal-lang/json_mapping.cr
-```
-
-2. Run `shards install`
-
-## Usage
-
-```crystal
-require "json_mapping"
-
-class Location
- JSON.mapping(
- lat: Float64,
- lng: Float64,
- )
-end
-
-class House
- JSON.mapping(
- address: String,
- location: {type: Location, nilable: true},
- )
-end
-
-house = House.from_json(%({"address": "Crystal Road 1234", "location": {"lat": 12.3, "lng": 34.5}}))
-house.address # => "Crystal Road 1234"
-house.location # => #
-house.to_json # => %({"address":"Crystal Road 1234","location":{"lat":12.3,"lng":34.5}})
-
-houses = Array(House).from_json(%([{"address": "Crystal Road 1234", "location": {"lat": 12.3, "lng": 34.5}}]))
-houses.size # => 1
-houses.to_json # => %([{"address":"Crystal Road 1234","location":{"lat":12.3,"lng":34.5}}])
-```
-
-## Contributing
-
-1. Fork it ()
-2. Create your feature branch (`git checkout -b my-new-feature`)
-3. Commit your changes (`git commit -am 'Add some feature'`)
-4. Push to the branch (`git push origin my-new-feature`)
-5. Create a new Pull Request
diff --git a/lib/json_mapping/lib b/lib/json_mapping/lib
deleted file mode 120000
index a96aa0ea..00000000
--- a/lib/json_mapping/lib
+++ /dev/null
@@ -1 +0,0 @@
-..
\ No newline at end of file
diff --git a/lib/json_mapping/shard.yml b/lib/json_mapping/shard.yml
deleted file mode 100644
index c42be08f..00000000
--- a/lib/json_mapping/shard.yml
+++ /dev/null
@@ -1,9 +0,0 @@
-name: json_mapping
-version: 0.1.1
-
-authors:
- - Brian J. Cardiff
-
-crystal: ">= 0.34.0, < 2.0.0"
-
-license: MIT
diff --git a/lib/json_mapping/src/json_mapping.cr b/lib/json_mapping/src/json_mapping.cr
deleted file mode 100644
index b7fee061..00000000
--- a/lib/json_mapping/src/json_mapping.cr
+++ /dev/null
@@ -1,259 +0,0 @@
-require "json"
-
-module JSON
- module Mapping
- VERSION = "0.1.1"
- end
-
- # The `JSON.mapping` macro defines how an object is mapped to JSON.
- #
- # ### Example
- #
- # ```
- # require "json_mapping"
- #
- # class Location
- # JSON.mapping(
- # lat: Float64,
- # lng: Float64,
- # )
- # end
- #
- # class House
- # JSON.mapping(
- # address: String,
- # location: {type: Location, nilable: true},
- # )
- # end
- #
- # house = House.from_json(%({"address": "Crystal Road 1234", "location": {"lat": 12.3, "lng": 34.5}}))
- # house.address # => "Crystal Road 1234"
- # house.location # => #
- # house.to_json # => %({"address":"Crystal Road 1234","location":{"lat":12.3,"lng":34.5}})
- #
- # houses = Array(House).from_json(%([{"address": "Crystal Road 1234", "location": {"lat": 12.3, "lng": 34.5}}]))
- # houses.size # => 1
- # houses.to_json # => %([{"address":"Crystal Road 1234","location":{"lat":12.3,"lng":34.5}}])
- # ```
- #
- # ### Usage
- #
- # `JSON.mapping` must receive a series of named arguments, or a named tuple literal, or a hash literal,
- # whose keys will define Crystal properties.
- #
- # The value of each key can be a type. Primitive types (numbers, string, boolean and nil)
- # are supported, as well as custom objects which use `JSON.mapping` or define a `new` method
- # that accepts a `JSON::PullParser` and returns an object from it. Union types are supported,
- # if multiple types in the union can be mapped from the JSON, it is undefined which one will be chosen.
- #
- # The value can also be another hash literal with the following options:
- # * **type**: (required) the type described above (you can use `JSON::Any` too)
- # * **key**: the property name in the JSON document (as opposed to the property name in the Crystal code)
- # * **nilable**: if `true`, the property can be `Nil`. Passing `T?` as a type has the same effect.
- # * **default**: value to use if the property is missing in the JSON document, or if it's `null` and `nilable` was not set to `true`. If the default value creates a new instance of an object (for example `[1, 2, 3]` or `SomeObject.new`), a different instance will be used each time a JSON document is parsed.
- # * **emit_null**: if `true`, emits a `null` value for nilable properties (by default nulls are not emitted)
- # * **converter**: specify an alternate type for parsing and generation. The converter must define `from_json(JSON::PullParser)` and `to_json(value, JSON::Builder)` as class methods. Examples of converters are `Time::Format` and `Time::EpochConverter` for `Time`.
- # * **root**: assume the value is inside a JSON object with a given key (see `Object.from_json(string_or_io, root)`)
- # * **setter**: if `true`, will generate a setter for the variable, `true` by default
- # * **getter**: if `true`, will generate a getter for the variable, `true` by default
- # * **presence**: if `true`, a `{{key}}_present?` method will be generated when the key was present (even if it has a `null` value), `false` by default
- #
- # This macro by default defines getters and setters for each variable (this can be overrided with *setter* and *getter*).
- # The mapping doesn't define a constructor accepting these variables as arguments, but you can provide an overload.
- #
- # The macro basically defines a constructor accepting a `JSON::PullParser` that reads from
- # it and initializes this type's instance variables. It also defines a `to_json(JSON::Builder)` method
- # by invoking `to_json(JSON::Builder)` on each of the properties (unless a converter is specified, in
- # which case `to_json(value, JSON::Builder)` is invoked).
- #
- # This macro also declares instance variables of the types given in the mapping.
- #
- # If *strict* is `true`, unknown properties in the JSON
- # document will raise a parse exception. The default is `false`, so unknown properties
- # are silently ignored.
- macro mapping(_properties_, strict = false)
- {% for key, value in _properties_ %}
- {% _properties_[key] = {type: value} unless value.is_a?(HashLiteral) || value.is_a?(NamedTupleLiteral) %}
- {% end %}
-
- {% for key, value in _properties_ %}
- {% _properties_[key][:key_id] = key.id.gsub(/\?$/, "") %}
- {% end %}
-
- {% for key, value in _properties_ %}
- @{{value[:key_id]}} : {{value[:type]}}{{ (value[:nilable] ? "?" : "").id }}
-
- {% if value[:setter] == nil ? true : value[:setter] %}
- def {{value[:key_id]}}=(_{{value[:key_id]}} : {{value[:type]}}{{ (value[:nilable] ? "?" : "").id }})
- @{{value[:key_id]}} = _{{value[:key_id]}}
- end
- {% end %}
-
- {% if value[:getter] == nil ? true : value[:getter] %}
- def {{key.id}} : {{value[:type]}}{{ (value[:nilable] ? "?" : "").id }}
- @{{value[:key_id]}}
- end
- {% end %}
-
- {% if value[:presence] %}
- @{{value[:key_id]}}_present : Bool = false
-
- def {{value[:key_id]}}_present?
- @{{value[:key_id]}}_present
- end
- {% end %}
- {% end %}
-
- def initialize(%pull : ::JSON::PullParser)
- {% for key, value in _properties_ %}
- %var{key.id} = nil
- %found{key.id} = false
- {% end %}
-
- %location = %pull.location
- begin
- %pull.read_begin_object
- rescue exc : ::JSON::ParseException
- raise ::JSON::MappingError.new(exc.message, self.class.to_s, nil, *%location, exc)
- end
- until %pull.kind.end_object?
- %key_location = %pull.location
- key = %pull.read_object_key
- case key
- {% for key, value in _properties_ %}
- when {{value[:key] || value[:key_id].stringify}}
- %found{key.id} = true
- begin
- %var{key.id} =
- {% if value[:nilable] || value[:default] != nil %} %pull.read_null_or { {% end %}
-
- {% if value[:root] %}
- %pull.on_key!({{value[:root]}}) do
- {% end %}
-
- {% if value[:converter] %}
- {{value[:converter]}}.from_json(%pull)
- {% elsif value[:type].is_a?(Path) || value[:type].is_a?(Generic) %}
- {{value[:type]}}.new(%pull)
- {% else %}
- ::Union({{value[:type]}}).new(%pull)
- {% end %}
-
- {% if value[:root] %}
- end
- {% end %}
-
- {% if value[:nilable] || value[:default] != nil %} } {% end %}
- rescue exc : ::JSON::ParseException
- raise ::JSON::MappingError.new(exc.message, self.class.to_s, {{value[:key] || value[:key_id].stringify}}, *%key_location, exc)
- end
- {% end %}
- else
- {% if strict %}
- raise ::JSON::MappingError.new("Unknown JSON attribute: #{key}", self.class.to_s, nil, *%key_location, nil)
- {% else %}
- %pull.skip
- {% end %}
- end
- end
- %pull.read_next
-
- {% for key, value in _properties_ %}
- {% unless value[:nilable] || value[:default] != nil %}
- if %var{key.id}.nil? && !%found{key.id} && !::Union({{value[:type]}}).nilable?
- raise ::JSON::MappingError.new("Missing JSON attribute: {{(value[:key] || value[:key_id]).id}}", self.class.to_s, nil, *%location, nil)
- end
- {% end %}
-
- {% if value[:nilable] %}
- {% if value[:default] != nil %}
- @{{value[:key_id]}} = %found{key.id} ? %var{key.id} : {{value[:default]}}
- {% else %}
- @{{value[:key_id]}} = %var{key.id}
- {% end %}
- {% elsif value[:default] != nil %}
- @{{value[:key_id]}} = %var{key.id}.nil? ? {{value[:default]}} : %var{key.id}
- {% else %}
- @{{value[:key_id]}} = (%var{key.id}).as({{value[:type]}})
- {% end %}
-
- {% if value[:presence] %}
- @{{value[:key_id]}}_present = %found{key.id}
- {% end %}
- {% end %}
- end
-
- def to_json(json : ::JSON::Builder)
- json.object do
- {% for key, value in _properties_ %}
- _{{value[:key_id]}} = @{{value[:key_id]}}
-
- {% unless value[:emit_null] %}
- unless _{{value[:key_id]}}.nil?
- {% end %}
-
- json.field({{value[:key] || value[:key_id].stringify}}) do
- {% if value[:root] %}
- {% if value[:emit_null] %}
- if _{{value[:key_id]}}.nil?
- nil.to_json(json)
- else
- {% end %}
-
- json.object do
- json.field({{value[:root]}}) do
- {% end %}
-
- {% if value[:converter] %}
- if _{{value[:key_id]}}
- {{ value[:converter] }}.to_json(_{{value[:key_id]}}, json)
- else
- nil.to_json(json)
- end
- {% else %}
- _{{value[:key_id]}}.to_json(json)
- {% end %}
-
- {% if value[:root] %}
- {% if value[:emit_null] %}
- end
- {% end %}
- end
- end
- {% end %}
- end
-
- {% unless value[:emit_null] %}
- end
- {% end %}
- {% end %}
- end
- end
- end
-
- # This is a convenience method to allow invoking `JSON.mapping`
- # with named arguments instead of with a hash/named-tuple literal.
- macro mapping(**_properties_)
- ::JSON.mapping({{_properties_}})
- end
-
- class MappingError < ParseException
- getter klass : String
- getter attribute : String?
-
- def initialize(message : String?, @klass : String, @attribute : String?, line_number : Int32, column_number : Int32, cause)
- message = String.build do |io|
- io << message
- io << "\n parsing "
- io << klass
- if attribute = @attribute
- io << '#' << attribute
- end
- end
- super(message, line_number, column_number, cause)
- if cause
- @line_number, @column_number = cause.location
- end
- end
- end
-end
diff --git a/lib/webmock/.gitignore b/lib/webmock/.gitignore
deleted file mode 100644
index 0d6aead9..00000000
--- a/lib/webmock/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-.deps/
-.crystal/
-.deps.lock
diff --git a/lib/webmock/.travis.yml b/lib/webmock/.travis.yml
deleted file mode 100644
index 31f20e13..00000000
--- a/lib/webmock/.travis.yml
+++ /dev/null
@@ -1,7 +0,0 @@
-language: crystal
-crystal:
- - latest
- - nightly
-script:
- - crystal spec
- - crystal tool format --check
diff --git a/lib/webmock/LICENSE b/lib/webmock/LICENSE
deleted file mode 100644
index 434acd74..00000000
--- a/lib/webmock/LICENSE
+++ /dev/null
@@ -1,21 +0,0 @@
-The MIT License (MIT)
-
-Copyright (c) 2015 Manas Technology Solutions
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
diff --git a/lib/webmock/README.md b/lib/webmock/README.md
deleted file mode 100644
index 287c1ce2..00000000
--- a/lib/webmock/README.md
+++ /dev/null
@@ -1,135 +0,0 @@
-# webmock.cr
-
-[![Build Status](https://travis-ci.org/manastech/webmock.cr.svg?branch=master)](https://travis-ci.org/manastech/webmock.cr)
-
-Library for stubbing `HTTP::Client` requests in [Crystal](http://crystal-lang.org/).
-
-Inspired by [webmock ruby gem](https://github.com/bblimke/webmock).
-
-## Installation
-
-Add it to `shards.yml`:
-
-```yaml
-development_dependencies:
- webmock:
- github: manastech/webmock.cr
- branch: master
-```
-
-## Usage
-
-```crystal
-require "webmock"
-```
-
-By requiring `webmock` unregistered `HTTP::Client` requests will raise an exception.
-If you still want to execute real requests, do this:
-
-```crystal
-WebMock.allow_net_connect = true
-```
-
-### Stub request based on uri only and with the default response
-
-```crystal
-WebMock.stub(:any, "www.example.com")
-
-response = HTTP::Client.get("http://www.example.com")
-response.body #=> ""
-response.status_code #=> 200
-```
-
-### Stub requests based on method, uri, body, headers and custom response
-
-```crystal
-WebMock.stub(:post, "www.example.com/foo").
- with(body: "abc", headers: {"Content-Type" => "text/plain"}).
- to_return(status: 500, body: "oops", headers: {"X-Error" => "true"})
-
-response = HTTP::Client.post("http://www.example.com/foo",
- body: "abc",
- headers: HTTP::Headers{"Content-Type" => "text/plain"})
-response.status_code #=> 500
-response.body #=> "oops"
-response.headers["X-Error"] #=> "true"
-
-# Executing the same request gives the same response
-response = HTTP::Client.post("http://www.example.com/foo",
- body: "abc",
- headers: HTTP::Headers{"Content-Type" => "text/plain"})
-response.body #=> "oops"
-```
-
-### Stub requests based on query string
-
-```crystal
-WebMock.stub(:get, "www.example.com").
- with(query: {"page" => "1", "count" => "10"})
-
-response = HTTP::Client.get("http://www.example.com?count=10&page=1")
-response.status_code #=> 200
-```
-
-### Stub requests and provide a block for the response
-
-Your block will be called and passed the `HTTP::Request`, allowing you to construct a response dynamically based upon the request.
-
-```crystal
-WebMock.stub(:post, "www.example.com/foo").to_return do |request|
- headers = HTTP::Headers.new.merge!({ "Content-length" => request.body.to_s.length })
- HTTP::Client::Response.new(418, body: request.body.to_s.reverse, headers: headers)
-end
-
-response = HTTP::Client.post("http://www.example.com/foo",
- body: "abc",
- headers: HTTP::Headers{"Content-Type" => "text/plain"})
-response.status_code #=> 418
-response.body #=> "cba"
-response.headers["Content-length"] #=> "3"
-
-response = HTTP::Client.post("http://www.example.com/foo",
- body: "olleh",
- headers: HTTP::Headers{"Content-Type" => "text/plain"})
-response.status_code #=> 418
-response.body #=> "hello"
-response.headers["Content-length"] #=> "5"
-```
-
-### Resetting
-
-```crystal
-WebMock.reset
-```
-
-This clears all stubs and sets `allow_net_connect` to `false`.
-
-To execute this automatically before each spec, you can do:
-
-```crystal
-Spec.before_each &->WebMock.reset
-```
-
-Or, for individual specs you can use `WebMock.wrap` and a block to make sure `WebMock` is reset at the end of a spec:
-
-```crystal
-WebMock.wrap do
- WebMock.stub(:get, "www.example.com").to_return(body: "Example")
-
- HTTP::Client.get("http://www.example.com").body #=> "Example"
-end
-
-HTTP::Client.get("http://www.example.com") # Raises WebMock::NetConnectNotAllowedError
-```
-
-## Todo
-
-Bring more features found in the [webmock ruby gem](https://github.com/bblimke/webmock).
-
-## Contributing
-
-1. Fork it ( https://github.com/manastech/webmock.cr/fork )
-2. Create your feature branch (git checkout -b my-new-feature)
-3. Commit your changes (git commit -am 'Add some feature')
-4. Push to the branch (git push origin my-new-feature)
-5. Create a new Pull Request
diff --git a/lib/webmock/lib b/lib/webmock/lib
deleted file mode 120000
index a96aa0ea..00000000
--- a/lib/webmock/lib
+++ /dev/null
@@ -1 +0,0 @@
-..
\ No newline at end of file
diff --git a/lib/webmock/shard.yml b/lib/webmock/shard.yml
deleted file mode 100644
index 462b940b..00000000
--- a/lib/webmock/shard.yml
+++ /dev/null
@@ -1,8 +0,0 @@
-name: webmock
-version: 0.14.0
-
-crystal: ">= 0.36.0, < 2.0.0"
-
-authors:
- - Ary Borenszweig
-
diff --git a/lib/webmock/src/webmock.cr b/lib/webmock/src/webmock.cr
deleted file mode 100644
index ce5210d4..00000000
--- a/lib/webmock/src/webmock.cr
+++ /dev/null
@@ -1,48 +0,0 @@
-require "./**"
-
-module WebMock
- extend self
-
- @@allow_net_connect = false
- @@registry = StubRegistry.new
- @@callbacks = CallbackRegistry.new
-
- def wrap
- yield
- ensure
- reset
- end
-
- def stub(method, uri)
- @@registry.stub(method, uri)
- end
-
- def reset
- @@registry.reset
- @@allow_net_connect = false
- end
-
- def allow_net_connect=(@@allow_net_connect)
- end
-
- def allows_net_connect?
- @@allow_net_connect
- end
-
- def find_stub(request : HTTP::Request)
- @@registry.find_stub(request)
- end
-
- def callbacks
- @@callbacks
- end
-
- # :nodoc:
- def self.body(request : HTTP::Request)
- body = request.body.try(&.gets_to_end)
- if body
- request.body = IO::Memory.new(body)
- end
- body
- end
-end
diff --git a/lib/webmock/src/webmock/callback_registry.cr b/lib/webmock/src/webmock/callback_registry.cr
deleted file mode 100644
index bf0a41e7..00000000
--- a/lib/webmock/src/webmock/callback_registry.cr
+++ /dev/null
@@ -1,26 +0,0 @@
-class WebMock::CallbackRegistry
- getter callbacks
-
- def initialize
- @callbacks = Hash(Symbol, (HTTP::Request, HTTP::Client::Response -> Nil)).new
- end
-
- def reset
- @callbacks.clear
- end
-
- def add
- with self yield
- self
- end
-
- def after_live_request(&block : (HTTP::Request, HTTP::Client::Response) ->)
- @callbacks[:after_live_request] = block
- end
-
- def call(name, *args)
- if !@callbacks.empty?
- @callbacks[name].try &.call(*args)
- end
- end
-end
diff --git a/lib/webmock/src/webmock/core_ext.cr b/lib/webmock/src/webmock/core_ext.cr
deleted file mode 100644
index 11436ba5..00000000
--- a/lib/webmock/src/webmock/core_ext.cr
+++ /dev/null
@@ -1,45 +0,0 @@
-require "http/client"
-
-class HTTP::Request
- property scheme : String = "http"
-
- def full_uri
- "#{scheme}://#{headers["Host"]?}#{resource}"
- end
-end
-
-class HTTP::Client
- private def exec_internal(request : HTTP::Request)
- exec_internal(request, &.itself).tap do |response|
- response.consume_body_io
- response.headers.delete("Transfer-encoding")
- response.headers["Content-length"] = response.body.bytesize.to_s
- end
- end
-
- private def exec_internal(request : HTTP::Request, &block : Response -> T) : T forall T
- request.scheme = "https" if tls?
- request.headers["Host"] = host_header unless request.headers.has_key?("Host")
- run_before_request_callbacks(request)
-
- stub = WebMock.find_stub(request)
- return yield(stub.exec(request)) if stub
- raise WebMock::NetConnectNotAllowedError.new(request) unless WebMock.allows_net_connect?
-
- request.headers["User-agent"] ||= "Crystal"
- request.to_io(io)
- io.flush
-
- result = nil
-
- HTTP::Client::Response.from_io(io, request.ignore_body?) do |response|
- result = yield(response)
- close unless response.keep_alive?
- WebMock.callbacks.call(:after_live_request, request, response)
- end
-
- raise "Unexpected end of response" unless result.is_a?(T)
-
- result
- end
-end
diff --git a/lib/webmock/src/webmock/net_connect_not_allowed_error.cr b/lib/webmock/src/webmock/net_connect_not_allowed_error.cr
deleted file mode 100644
index 2036dab0..00000000
--- a/lib/webmock/src/webmock/net_connect_not_allowed_error.cr
+++ /dev/null
@@ -1,82 +0,0 @@
-class WebMock::NetConnectNotAllowedError < Exception
- def initialize(request : HTTP::Request)
- super(help_message(request))
- end
-
- private def help_message(request)
- String.build do |io|
- io << "Real HTTP connections are disabled. "
- io << "Unregistered request: "
- signature(request, io)
- io << "\n\n"
- io << "You can stub this request with the following snippet:"
- io << "\n\n"
- stubbing_instructions(request, io)
- io << "\n\n"
- end
- end
-
- private def signature(request, io)
- io << request.method << " "
- request_uri_to_s request, io
- if request.body
- io << " with body "
- WebMock.body(request).inspect(io)
- end
- io << " with headers "
- headers_to_s request.headers, io
- end
-
- private def stubbing_instructions(request, io)
- # For the instructions we remove these two headers because they are automatically
- # included in HTTP::Client requests
- headers = request.headers.dup
- headers.delete("Content-Length")
- headers.delete("Connection")
- headers.delete("Host")
-
- io << "WebMock.stub(:" << request.method.downcase << ", "
- io << '"'
- request_uri_to_s request, io
- io << %[").]
- io.puts
-
- if request.body && !headers.empty?
- io << " with("
-
- if request.body
- io << "body: "
- WebMock.body(request).inspect(io)
- io << ", " unless headers.empty?
- end
-
- unless headers.empty?
- io << "headers: "
- headers_to_s headers, io
- end
- io << ")."
- io.puts
- end
-
- io << %[ to_return(body: "")]
- end
-
- private def request_uri_to_s(request, io)
- io << request.scheme
- io << "://"
- io << request.headers["Host"]
- io << request.path
- io << "?#{request.query}" if request.query
- end
-
- private def headers_to_s(headers, io)
- io << "{"
- headers.each_with_index do |(key, values), index|
- io << ", " if index > 0
- key.inspect(io)
- io << " => "
- values.join(", ").inspect(io)
- end
- io << "}"
- end
-end
diff --git a/lib/webmock/src/webmock/stub.cr b/lib/webmock/src/webmock/stub.cr
deleted file mode 100644
index acdd3d71..00000000
--- a/lib/webmock/src/webmock/stub.cr
+++ /dev/null
@@ -1,135 +0,0 @@
-class WebMock::Stub
- @method : String
- @uri : URI | Regex
- @expected_headers : HTTP::Headers?
- @calls = 0
- @body_io : IO?
-
- def initialize(method : Symbol | String, uri : String | Regex)
- @method = method.to_s.upcase
- @uri = uri.is_a?(String) ? parse_uri(uri) : uri
-
- # For to_return
- @status = 200
- @body = ""
- @headers = HTTP::Headers{"Content-length" => "0", "Connection" => "close"}
-
- @block = Proc(HTTP::Request, HTTP::Client::Response).new do |_request|
- HTTP::Client::Response.new(@status, body: @body, headers: @headers, body_io: @body_io)
- end
- end
-
- def with(query : Hash(String, String)? = nil, body : String? = nil, headers = nil)
- @expected_query = query
- @expected_body = body
- @expected_headers = HTTP::Headers.new.merge!(headers) if headers
- self
- end
-
- def to_return(body : String? = "", status = 200, headers = nil)
- @body = body
- @body_io = nil
- @status = status
- @headers.delete("Transfer-encoding")
- @headers["Content-length"] = body.size.to_s
- @headers.merge!(headers) if headers
- self
- end
-
- def to_return(body_io : IO, status = 200, headers = nil)
- @body = nil
- @body_io = body_io
- @status = status
- @headers.delete("Content-length")
- @headers["Transfer-encoding"] = "chunked"
- @headers.merge!(headers) if headers
- self
- end
-
- def to_return(&block : HTTP::Request -> HTTP::Client::Response)
- @block = block
- self
- end
-
- def matches?(request)
- matches_method?(request) &&
- matches_uri?(request) &&
- matches_body?(request) &&
- matches_headers?(request)
- end
-
- def matches_uri?(request)
- case uri = @uri
- when URI
- matches_scheme?(request, uri) &&
- matches_host?(request, uri) &&
- matches_path?(request, uri)
- when Regex
- uri =~ request.full_uri
- end
- end
-
- def matches_method?(request)
- return true if @method == "ANY"
-
- @method == request.method
- end
-
- def matches_scheme?(request, uri)
- uri.scheme == request.scheme
- end
-
- def matches_host?(request, uri)
- host_uri = parse_uri(request.headers["Host"])
- host_uri.host == uri.host && host_uri.port == uri.port
- end
-
- def matches_path?(request, uri)
- uri_path = uri.path.presence || "/"
- uri_query = uri.query
-
- request_uri = parse_uri(request.resource)
- request_path = request_uri.path.presence || "/"
- request_query = request_uri.query
-
- request_query = HTTP::Params.parse(request_query || "")
- uri_query = HTTP::Params.parse(uri_query || "")
-
- @expected_query.try &.each do |key, value|
- uri_query.add(key.to_s, value.to_s)
- end
- request_path == uri_path && request_query == uri_query
- end
-
- def matches_body?(request)
- @expected_body ? @expected_body == WebMock.body(request) : true
- end
-
- def matches_headers?(request)
- expected_headers = @expected_headers
- return true unless expected_headers
-
- expected_headers.each do |key, _|
- request_value = request.headers[key]?
- expected_value = expected_headers[key]?
- return false unless request_value.to_s == expected_value.to_s
- end
-
- true
- end
-
- def exec(request)
- @calls += 1
- @block.call(request)
- end
-
- def calls
- @calls
- end
-
- private def parse_uri(uri_string)
- uri = URI.parse(uri_string)
- uri = URI.parse("http://#{uri_string}") unless uri.host
- uri
- end
-end
diff --git a/lib/webmock/src/webmock/stub_registry.cr b/lib/webmock/src/webmock/stub_registry.cr
deleted file mode 100644
index 789660c9..00000000
--- a/lib/webmock/src/webmock/stub_registry.cr
+++ /dev/null
@@ -1,19 +0,0 @@
-struct WebMock::StubRegistry
- def initialize
- @stubs = [] of Stub
- end
-
- def stub(method, uri)
- stub = Stub.new(method, uri)
- @stubs << stub
- stub
- end
-
- def reset
- @stubs.clear
- end
-
- def find_stub(request)
- @stubs.find &.matches?(request)
- end
-end
diff --git a/script/acceptance b/script/acceptance
index 35dc63c6..5fbc421a 100755
--- a/script/acceptance
+++ b/script/acceptance
@@ -10,4 +10,8 @@ PURPLE='\033[0;35m'
# set the working directory to the root of the project
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )"
+if [[ "$CI" == "true" ]]; then
+ source script/ci-env
+fi
+
crystal run spec/acceptance/acceptance.cr
diff --git a/script/bootstrap b/script/bootstrap
index 996ce879..e35134ed 100755
--- a/script/bootstrap
+++ b/script/bootstrap
@@ -12,6 +12,21 @@ YELLOW='\033[0;33m'
# set the working directory to the root of the project
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )"
+VENDOR_DIR="$DIR/vendor"
+SHARDS_CACHE_PATH="$VENDOR_DIR/.cache/shards"
+SHARDS_INSTALL_PATH="$VENDOR_DIR/shards/install"
+
+if [[ "$@" == *"--ci"* ]]; then
+ source script/ci-env
+fi
+
+crystal_path_var="$(crystal env CRYSTAL_PATH)"
+# if the crystal_path_var does not contain 'vendor/shards/install' then we need to add it to the CRYSTAL_PATH
+if [[ "$crystal_path_var" != *"vendor/shards/install"* ]]; then
+ echo -e "๐ก ${YELLOW}WARNING${OFF}: This project uses a custom vendoring strategy. Please update your ${PURPLE}CRYSTAL_PATH${OFF} env var to also check the ${PURPLE}vendor/shards/install${OFF} dir relative to the root of this (and other) crystal projects for builds to work properly. Example:\n"
+ echo -e "${PURPLE}export CRYSTAL_PATH=\"vendor/shards/install:$(crystal env CRYSTAL_PATH)\"${OFF}\n"
+ echo -e "It is suggested to add this to your ${PURPLE}.bashrc${OFF} or ${PURPLE}.zshrc${OFF} file so you only have to update it once and then can forget about it.\n"
+fi
# check to ensure both crystal and shards are installed
if ! [ -x "$(command -v crystal)" ]; then
@@ -64,7 +79,18 @@ if [[ ! -z "$compatability_warning" && "$SUPPRESS_BOOTSTRAP_WARNINGS" != "true"
fi
script/preinstall
+script/unzipper
+
+# for now, I think I can get away with `--ci` passing in `--skip-postinstall`. This may change one day when I actually need to run the postinstall command for deps in ci
+ci_flags=""
+if [[ "$@" == *"--ci"* ]]; then
+ ci_flags="--skip-postinstall --skip-executables"
+fi
+
+# install the shards
+SHARDS_CACHE_PATH="$SHARDS_CACHE_PATH" SHARDS_INSTALL_PATH="$SHARDS_INSTALL_PATH" shards install --local --frozen $ci_flags $@
-SHARDS_CACHE_PATH="$DIR/.cache/shards" shards install --frozen $@
+# shards install often wipes out our custom shards sha256 file so we need to recompute it if they are gone
+script/compute-dep-shas
-script/postinstall
+script/postinstall $@
diff --git a/script/build b/script/build
new file mode 100755
index 00000000..7ae5ee09
--- /dev/null
+++ b/script/build
@@ -0,0 +1,31 @@
+#!/bin/bash
+
+set -e
+
+# COLORS
+OFF='\033[0m'
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+BLUE='\033[0;34m'
+PURPLE='\033[0;35m'
+
+# set the working directory to the root of the project
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )"
+VENDOR_DIR="$DIR/vendor"
+SHARDS_CACHE_PATH="$VENDOR_DIR/.cache/shards"
+SHARDS_INSTALL_PATH="$VENDOR_DIR/shards/install"
+
+if [[ "$CI" == "true" ]]; then
+ source script/ci-env
+fi
+
+# if the --production flag is passed or the CRYSTAL_ENV environment variable is set to production,
+# always ensure the CRYSTAL_PATH is set with vendored shards
+if [[ "$@" == *"--production"* ]] || [[ "$CRYSTAL_ENV" == "production" ]]; then
+ echo "๐จ setting CRYSTAL_PATH to $VENDOR_DIR/shards/install:$(crystal env CRYSTAL_PATH)"
+ export CRYSTAL_PATH="vendor/shards/install:$(crystal env CRYSTAL_PATH)"
+fi
+
+echo -e "๐จ ${BLUE}building in ${PURPLE}release${BLUE} mode${OFF}"
+SHARDS_CACHE_PATH="$SHARDS_CACHE_PATH" SHARDS_INSTALL_PATH="$SHARDS_INSTALL_PATH" shards build --production --release --progress --debug --error-trace
+echo -e "๐ฆ ${GREEN}build complete${OFF}"
diff --git a/script/ci-env b/script/ci-env
new file mode 100755
index 00000000..ffa9e579
--- /dev/null
+++ b/script/ci-env
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+echo "running ci-env script"
+
+export CRYSTAL_PATH="vendor/shards/install:$(crystal env CRYSTAL_PATH)"
diff --git a/script/compute-dep-shas b/script/compute-dep-shas
new file mode 100755
index 00000000..e609dd50
--- /dev/null
+++ b/script/compute-dep-shas
@@ -0,0 +1,31 @@
+#!/bin/bash
+
+# set the working directory to the root of the project
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )"
+VENDOR_DIR="$DIR/vendor"
+SHARDS_CACHE_PATH="$VENDOR_DIR/.cache/shards"
+SHARDS_INSTALL_PATH="$VENDOR_DIR/shards/install"
+SHARDS_CACHED="$VENDOR_DIR/shards/cache"
+
+SHARD_SHA_FILE=".shard.vendor.cache.sha256"
+
+file="vendor/shards/install/.shards.info"
+
+if [ -f "$VENDOR_DIR/shards/install/.shards.info" ]; then
+
+ # Use yq to parse the file and extract shard names and versions
+ shards=$(yq eval '.shards | to_entries | .[] | "\(.key)|\(.value.git)|\(.value.version)"' $file)
+
+ # Loop over each shard
+ echo "$shards" | while IFS= read -r shard; do
+ # Extract name and version
+ name=$(echo $shard | cut -d'|' -f1)
+ version=$(echo $shard | cut -d'|' -f3)
+
+ # if the shard sha256 file does not exist, try to compute and create it
+ if [ ! -f "$SHARDS_INSTALL_PATH/name/$SHARD_SHA_FILE" ]; then
+ shard_cache_sha=$(shasum -a 256 "$SHARDS_CACHED/$name-$version.shard" | cut -d' ' -f1)
+ cat > "$SHARDS_INSTALL_PATH/$name/$SHARD_SHA_FILE" <<< "$shard_cache_sha"
+ fi
+ done
+fi
diff --git a/script/format b/script/format
index f6621575..f4122a64 100755
--- a/script/format
+++ b/script/format
@@ -12,6 +12,10 @@ PURPLE='\033[0;35m'
# set the working directory to the root of the project
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )"
+if [[ "$CI" == "true" ]]; then
+ source script/ci-env
+fi
+
echo -e "๐งน ${BLUE}formatting ${PURPLE}crystal${BLUE} files...${OFF}"
crystal tool format $@
diff --git a/script/lint b/script/lint
index 415c08f5..efa8f30c 100755
--- a/script/lint
+++ b/script/lint
@@ -12,6 +12,10 @@ PURPLE='\033[0;35m'
# set the working directory to the root of the project
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )"
+if [[ "$CI" == "true" ]]; then
+ source script/ci-env
+fi
+
echo -e "๐๏ธ ${BLUE}linting ${PURPLE}crystal${BLUE} files..."
"$DIR/bin/ameba" $@
diff --git a/script/postinstall b/script/postinstall
index d807d896..f2831b76 100755
--- a/script/postinstall
+++ b/script/postinstall
@@ -2,28 +2,76 @@
# set the working directory to the root of the project
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )"
+VENDOR_DIR="$DIR/vendor"
+SHARDS_INSTALL_PATH="$VENDOR_DIR/shards/install"
+
+LINUX_VENDOR_DIR="$VENDOR_DIR/linux_x86_64/bin"
+DARWIN_VENDOR_DIR_X64="$VENDOR_DIR/darwin_x86_64/bin"
+DARWIN_VENDOR_DIR_ARM64="$VENDOR_DIR/darwin_arm64/bin"
+
+if [[ "$CI" == "true" ]]; then
+ source script/ci-env
+fi
mkdir -p "$DIR/bin"
-# ensure the ameba binary is built and available in the bin directory
-AMEBA_UP_TO_DATE=false
-# first, check the version of the ameba binary in the lock file
-AMEBA_VERSION=$(shards list | grep ameba | awk '{print $3}' | tr -d '()')
-
-# if the bin/ameba binary exists, check if it is the correct version
-if [ -f "$DIR/bin/ameba" ]; then
- CURRENT_VERSION=$("$DIR/bin/ameba" --version)
- if [ "$AMEBA_VERSION" = "$CURRENT_VERSION" ]; then
- AMEBA_UP_TO_DATE=true
- else
- echo "ameba binary is not up to date"
- echo "ameba version (./bin/ameba): $CURRENT_VERSION"
- echo "ameba version (shards list): $AMEBA_VERSION"
- AMEBA_UP_TO_DATE=false
+flags="$@"
+
+if [[ "$flags" == *"--ci"* ]]; then
+ echo $OSTYPE
+ echo $(uname -m)
+fi
+
+# Determine if the system is a Mac or Linux
+os="unknown"
+arch="unknown"
+if [[ "$OSTYPE" == "linux-gnu" ]]; then
+ os="linux"
+ if [[ "$flags" == *"--ci"* ]]; then
+ echo "OSTYPE: $OSTYPE"
+ echo "running in CI mode, copying vendored binaries to bin/ directory (linux)"
+ cp -r "$LINUX_VENDOR_DIR/." "$DIR/bin"
fi
+elif [[ "$OSTYPE" == "darwin"* ]]; then
+ os="mac"
+ arch=$(uname -m)
+ if [[ "$flags" == *"--ci"* ]]; then
+ echo "OSTYPE: $OSTYPE"
+ echo "ARCH: $arch"
+ echo "running in CI mode, copying vendored binaries to bin/ directory (mac)"
+ if [ "$arch" = "x86_64" ]; then
+ cp -r "$DARWIN_VENDOR_DIR_X64/." "$DIR/bin"
+ elif [ "$arch" = "arm64" ]; then
+ cp -r "$DARWIN_VENDOR_DIR_ARM64/" "$DIR/bin"
+ else
+ echo "Unknown architecture: $arch"
+ fi
+ fi
+else
+ os="unknown"
fi
-if [ "$AMEBA_UP_TO_DATE" = false ]; then
- echo "building ameba binary"
- cd "$DIR/lib/ameba" && shards build && cp bin/ameba "$DIR/bin/ameba" && cd "$DIR"
+if [[ ! "$@" == *"--production"* ]]; then
+ # ensure the ameba binary is built and available in the bin directory
+ AMEBA_UP_TO_DATE=false
+ # first, check the version of the ameba binary in the lock file
+ AMEBA_VERSION=$(SHARDS_INSTALL_PATH="$SHARDS_INSTALL_PATH" shards list | grep ameba | awk '{print $3}' | tr -d '()')
+
+ # if the bin/ameba binary exists, check if it is the correct version
+ if [ -f "$DIR/bin/ameba" ]; then
+ CURRENT_VERSION=$("$DIR/bin/ameba" --version)
+ if [ "$AMEBA_VERSION" = "$CURRENT_VERSION" ]; then
+ AMEBA_UP_TO_DATE=true
+ else
+ echo "ameba binary is not up to date"
+ echo "ameba version (./bin/ameba): $CURRENT_VERSION"
+ echo "ameba version (shards list): $AMEBA_VERSION"
+ AMEBA_UP_TO_DATE=false
+ fi
+ fi
+
+ if [ "$AMEBA_UP_TO_DATE" = false ]; then
+ echo "building ameba binary"
+ cd "$SHARDS_INSTALL_PATH/ameba" && shards build && cp bin/ameba "$DIR/bin/ameba" && cd "$DIR"
+ fi
fi
diff --git a/script/test b/script/test
index a9b4385d..396f1350 100755
--- a/script/test
+++ b/script/test
@@ -14,6 +14,10 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )"
echo -e "๐งช ${BLUE}running unit tests...${OFF}"
+if [[ "$CI" == "true" ]]; then
+ source script/ci-env
+fi
+
crystal spec
echo -e "โ
${GREEN}tests complete!${OFF}"
diff --git a/script/unzipper b/script/unzipper
new file mode 100755
index 00000000..f323ab3e
--- /dev/null
+++ b/script/unzipper
@@ -0,0 +1,92 @@
+#!/bin/bash
+
+# set the working directory to the root of the project
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )"
+VENDOR_DIR="$DIR/vendor"
+SHARDS_CACHE_PATH="$VENDOR_DIR/.cache/shards"
+SHARDS_INSTALL_PATH="$VENDOR_DIR/shards/install"
+SHARDS_CACHED="$VENDOR_DIR/shards/cache"
+
+SHARD_SHA_FILE=".shard.vendor.cache.sha256"
+
+TRASHDIR=$(mktemp -d /tmp/bootstrap.XXXXXXXXXXXXXXXXX)
+cleanup() {
+ rm -rf "$TRASHDIR"
+}
+trap cleanup EXIT
+
+mkdir -p "$SHARDS_INSTALL_PATH"
+mkdir -p "$SHARDS_CACHE_PATH/github.com"
+
+# check if the .shards.info file exists, if it doesn't this is a fresh bootstrapping
+if [ ! -f "$VENDOR_DIR/shards/install/.shards.info" ]; then
+ # if no .shards.info file found, this must be a fresh bootstrapping
+
+ # iterate over all the cached shards in the vendor/shards/cache directory
+ for shard in $(find "$SHARDS_CACHED" -type f -maxdepth 1); do
+
+ # unzip the file into the TRASHDIR
+ unzip -q -o "$shard" -d "$TRASHDIR"
+
+ # get the only name of the dir in the TRASHDIR
+ shard_name=$(ls "$TRASHDIR/shard/")
+
+ # clear up the shard in the install dir if it exists
+ rm -rf "$SHARDS_INSTALL_PATH/$shard_name"
+
+ # move the shard and cache directories to the correct location
+ mv -f "$TRASHDIR/shard/"* "$SHARDS_INSTALL_PATH/" 2>/dev/null || true
+ mv -f "$TRASHDIR/cache/"* "$SHARDS_CACHE_PATH/github.com/" 2>/dev/null || true
+
+ # cleanup the TRASHDIR
+ rm -rf "$TRASHDIR/shard"
+ rm -rf "$TRASHDIR/cache"
+
+ shard_cache_sha=$(shasum -a 256 "$shard" | cut -d' ' -f1)
+
+ # write the new sha to the $SHARD_SHA_FILE file
+ cat > "$SHARDS_INSTALL_PATH/$shard_name/$SHARD_SHA_FILE" <<< "$shard_cache_sha"
+ done
+
+else
+ # if found .shards.info file, this must be a bootstrap re-run - we will check if the shards have changed by comparing the sha256 of the cached shard and the sha256 of the current shard
+
+ file="vendor/shards/install/.shards.info"
+
+ # Use yq to parse the file and extract shard names and versions
+ shards=$(yq eval '.shards | to_entries | .[] | "\(.key)|\(.value.git)|\(.value.version)"' $file)
+
+ # Loop over each shard
+ echo "$shards" | while IFS= read -r shard; do
+ # Extract name and version
+ name=$(echo $shard | cut -d'|' -f1)
+ version=$(echo $shard | cut -d'|' -f3)
+
+ shard_cache_sha=$(shasum -a 256 "$SHARDS_CACHED/$name-$version.shard" | cut -d' ' -f1)
+ shard_current_sha=""
+ if [ -f "$SHARDS_INSTALL_PATH/$name/$SHARD_SHA_FILE" ]; then
+ shard_current_sha=$(cat "$SHARDS_INSTALL_PATH/$name/$SHARD_SHA_FILE")
+ fi
+
+ if [ "$shard_cache_sha" != "$shard_current_sha" ]; then
+ echo "shard $name $version has changed, updating"
+
+ # unzip the file into the TRASHDIR
+ unzip -q -o "$SHARDS_CACHED/$name-$version.shard" -d "$TRASHDIR"
+
+ # clear up the shard in the install dir if it exists
+ rm -rf "$SHARDS_INSTALL_PATH/$name"
+
+ # move the shard and cache directories to the correct location
+ mv -f "$TRASHDIR/shard/"* "$SHARDS_INSTALL_PATH/" 2>/dev/null || true
+ mv -f "$TRASHDIR/cache/"* "$SHARDS_CACHE_PATH/github.com/" 2>/dev/null || true
+
+ # write the new sha to the $SHARD_SHA_FILE file
+ cat > "$SHARDS_INSTALL_PATH/$name/$SHARD_SHA_FILE" <<< "$shard_cache_sha"
+
+ # cleanup the TRASHDIR
+ rm -rf "$TRASHDIR/shard"
+ rm -rf "$TRASHDIR/cache"
+ fi
+ done
+fi
diff --git a/script/update b/script/update
index f344f868..2f7244ce 100755
--- a/script/update
+++ b/script/update
@@ -11,13 +11,17 @@ PURPLE='\033[0;35m'
# set the working directory to the root of the project
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )"
+VENDOR_DIR="$DIR/vendor"
+SHARDS_CACHE_PATH="$VENDOR_DIR/.cache/shards"
+SHARDS_INSTALL_PATH="$VENDOR_DIR/shards/install"
echo -e "๐ฆ ${BLUE}Running ${PURPLE}shards update${BLUE} to update all dependencies${OFF}"
script/preinstall
-SHARDS_CACHE_PATH="$DIR/.cache/shards" shards update --skip-postinstall --skip-executables $@
+SHARDS_CACHE_PATH="$SHARDS_CACHE_PATH" SHARDS_INSTALL_PATH="$SHARDS_INSTALL_PATH" shards update $@
+script/zipper
script/postinstall
echo -e "โ
${GREEN}All dependencies have been updated!${OFF}"
diff --git a/script/zipper b/script/zipper
new file mode 100755
index 00000000..977ccaaa
--- /dev/null
+++ b/script/zipper
@@ -0,0 +1,59 @@
+#!/bin/bash
+
+# set the working directory to the root of the project
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )"
+VENDOR_DIR="$DIR/vendor"
+SHARDS_CACHE_PATH="$VENDOR_DIR/.cache/shards"
+SHARDS_INSTALL_PATH="$VENDOR_DIR/shards/install"
+SHARDS_CACHED="$VENDOR_DIR/shards/cache"
+
+file="vendor/shards/install/.shards.info"
+
+# Function to parse owner and repo from URL
+parse_owner_repo() {
+ local url=$1
+ # Use awk to extract the owner and repo name
+ echo $url | awk -F'/' '{print $(NF-1)"/"$(NF)}' | sed 's/.git$//'
+}
+
+if ! [ -x "$(command -v yq)" ]; then
+ echo -e "โ ${RED}Error${OFF}: yq is not installed"
+ echo "Please install yq -> https://github.com/mikefarah/yq"
+ exit 1
+fi
+
+TRASHDIR=$(mktemp -d /tmp/bootstrap.XXXXXXXXXXXXXXXXX)
+cleanup() {
+ rm -rf "$TRASHDIR"
+}
+trap cleanup EXIT
+
+# Use yq to parse the file and extract shard names and versions
+shards=$(yq eval '.shards | to_entries | .[] | "\(.key)|\(.value.git)|\(.value.version)"' $file)
+
+# Loop over each shard
+echo "$shards" | while IFS= read -r shard; do
+ # Extract name and version
+ name=$(echo $shard | cut -d'|' -f1)
+ git_url=$(echo $shard | cut -d'|' -f2)
+ owner_repo=$(parse_owner_repo $git_url)
+ owner=$(echo $owner_repo | cut -d'/' -f1)
+ repo=$(echo $owner_repo | cut -d'/' -f2)
+ version=$(echo $shard | cut -d'|' -f3)
+
+ mkdir -p "$TRASHDIR/$name-$version.shard/shard/$name"
+ mkdir -p "$TRASHDIR/$name-$version.shard/cache/$owner/$repo.git"
+ cp -r "$SHARDS_INSTALL_PATH/$name/"* "$TRASHDIR/$name-$version.shard/shard/$name" 2>/dev/null || true
+ cp -r "$SHARDS_CACHE_PATH/github.com/$owner/$repo.git/"* "$TRASHDIR/$name-$version.shard/cache/$owner/$repo.git"
+
+ echo '{"name": "'$name'", "version": "'$version'", "repository": "'$owner/$repo'"}' > "$TRASHDIR/$name-$version.shard/metadata.json"
+
+ # Change to the temporary directory and zip the shard and cache directories
+ (cd "$TRASHDIR/$name-$version.shard" && zip -q -r "$TRASHDIR/$name-$version.zip" shard cache metadata.json -x "*.shard.vendor.cache.sha256")
+
+ # Move the zip to the cache
+ mkdir -p "$SHARDS_CACHED"
+ mv "$TRASHDIR/$name-$version.zip" "$SHARDS_CACHED/$name-$version.shard"
+
+ # echo "cached $name $version"
+done
diff --git a/vendor/darwin_arm64/bin/ameba b/vendor/darwin_arm64/bin/ameba
new file mode 100755
index 00000000..92e80f27
Binary files /dev/null and b/vendor/darwin_arm64/bin/ameba differ
diff --git a/vendor/darwin_x86_64/bin/ameba b/vendor/darwin_x86_64/bin/ameba
new file mode 100755
index 00000000..24a8f36b
Binary files /dev/null and b/vendor/darwin_x86_64/bin/ameba differ
diff --git a/vendor/linux_x86_64/bin/ameba b/vendor/linux_x86_64/bin/ameba
new file mode 100755
index 00000000..66f9d744
Binary files /dev/null and b/vendor/linux_x86_64/bin/ameba differ
diff --git a/vendor/shards/cache/ameba-1.6.1.shard b/vendor/shards/cache/ameba-1.6.1.shard
new file mode 100644
index 00000000..a04a60a2
Binary files /dev/null and b/vendor/shards/cache/ameba-1.6.1.shard differ
diff --git a/vendor/shards/cache/halite-0.12.0.shard b/vendor/shards/cache/halite-0.12.0.shard
new file mode 100644
index 00000000..5eb7ba57
Binary files /dev/null and b/vendor/shards/cache/halite-0.12.0.shard differ
diff --git a/vendor/shards/cache/json_mapping-0.1.1.shard b/vendor/shards/cache/json_mapping-0.1.1.shard
new file mode 100644
index 00000000..98f41bf4
Binary files /dev/null and b/vendor/shards/cache/json_mapping-0.1.1.shard differ
diff --git a/vendor/shards/cache/webmock-0.14.0.shard b/vendor/shards/cache/webmock-0.14.0.shard
new file mode 100644
index 00000000..81bf9ce0
Binary files /dev/null and b/vendor/shards/cache/webmock-0.14.0.shard differ