Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor to keep feature simple #36

Merged
merged 8 commits into from
Aug 31, 2018
71 changes: 47 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
[![Tag](https://img.shields.io/github/tag/icyleaf/halite.svg)](https://github.com/icyleaf/halite/blob/master/CHANGELOG.md)
[![Build Status](https://img.shields.io/circleci/project/github/icyleaf/halite/master.svg?style=flat)](https://circleci.com/gh/icyleaf/halite)

Crystal HTTP Requests with a chainable REST API, built-in sessions and loggers written by [Crystal](https://crystal-lang.org/).
HTTP Requests with a chainable REST API, built-in sessions and loggers written by [Crystal](https://crystal-lang.org/).
Inspired from the **awesome** Ruby's [HTTP](https://github.com/httprb/http)/[RESTClient](https://github.com/rest-client/rest-client) gem
and Python's [requests](https://github.com/requests/requests).

Build in crystal version >= `v0.25.0`, this document valid in latest commit.
Build in crystal version >= `v0.25.0`, this document valid with latest commit.

## Index

Expand Down Expand Up @@ -41,6 +41,7 @@ Build in crystal version >= `v0.25.0`, this document valid in latest commit.
- [Error Handling](#error-handling)
- [Raise for status code](#raise-for-status-code)
- [Advanced Usage](#advanced-usage)
- [Configuring](#configuring)
- [Sessions](#sessions)
- [Logging](#logging)
- [JSON-formatted logging](#json-formatted-logging)
Expand Down Expand Up @@ -326,6 +327,17 @@ r.history
# ]
```

**NOTE**: It contains the `Response` object if you use `history` and HTTP was not a `30x`, For example:

```crystal
r = Halite.get("http://httpbin.org/get")
r.history.size # => 0

r = Halite.follow
.get("http://httpbin.org/get")
r.history.size # => 1
```

#### Timeout

By default, the Halite does not enforce timeout on a request.
Expand Down Expand Up @@ -412,7 +424,7 @@ we can inherit `Halite::MimeTypes::Adapter` make our adapter:

```crystal
# Define a MIME type adapter
class YAMLAdapter < Halite::MimeTypes::Adapter
class YAMLAdapter < Halite::MimeType::Adapter
def decode(string)
YAML.parse(string)
end
Expand All @@ -423,9 +435,7 @@ class YAMLAdapter < Halite::MimeTypes::Adapter
end

# Register to Halite to invoke
Halite::MimeTypes.register_adapter "application/x-yaml", YAMLAdapter.new
Halite::MimeTypes.register_alias "application/x-yaml", "yaml"
Halite::MimeTypes.register_alias "application/x-yaml", "yml"
Halite::MimeType.register YAMLAdapter.new, "application/x-yaml", "yaml", "yml"

# Test it!
r = Halite.get "https://raw.githubusercontent.com/icyleaf/halite/master/shard.yml"
Expand Down Expand Up @@ -485,26 +495,39 @@ end

## Advanced Usage

### Sessions

As like [requests.Session()](http://docs.python-requests.org/en/master/user/advanced/#session-objects), Halite built-in session by default.
### Configuring

Let's persist some cookies across requests:
Halite provides a traditional way to instance client, and you can configure any chainable methods with block:

```crystal
client = Halite::Client.new
# Or configure it
client = Halite::Client.new do
# Set basic auth
basic_auth "name", "foo"
basic_auth "username", "password"

# Enable logging
logging true

# Set read timeout to one minute
timeout(read: 1.minutes)
# 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")
```

### 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
Expand All @@ -528,7 +551,7 @@ 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:

- `logger`: Instance your `Halite::Features::Logger::Abstract`, check [Use the custom logger](#use-the-custom-logger).
- `logger`: Instance your `Halite::Logger::Abstract`, check [Use the custom logger](#use-the-custom-logger).
- `format`: Outputing 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)
Expand Down Expand Up @@ -588,11 +611,11 @@ Halite.logger(format: "json", file: "logs/halite.log")

#### Use the custom logger

Creating the custom logger by integration `Halite::Features::Logger::Abstract` abstract class.
Creating the custom logger by integration `Halite::Logger::Abstract` abstract class.
Here has two methods must be implement: `#request` and `#response`.

```crystal
class CustomLogger < Halite::Features::Logger::Abstract
class CustomLogger < Halite::Logging::Abstract
def request(request)
@logger.info "| >> | %s | %s %s" % [request.verb, request.uri, request.body]
end
Expand All @@ -603,7 +626,7 @@ class CustomLogger < Halite::Features::Logger::Abstract
end

# Add to adapter list (optional)
Halite::Logger.register_adapter "custom", CustomLogger.new
Halite::Logging.register "custom", CustomLogger.new

Halite.logger(logger: CustomLogger.new)
.get("http://httpbin.org/get", params: {name: "foobar"})
Expand All @@ -623,7 +646,7 @@ in your HTTP client and allowing you to monitor outgoing requests, and incoming

Avaiabled features:

- logger (Cool, aha!)
- logging (Yes, logging is based on feature, cool, aha!)

#### Write a simple feature

Expand All @@ -645,7 +668,7 @@ class RequestMonister < Halite::Feature
request
end

Halite::Features.register "request_monster", self
Halite.register_feature "request_monster", self
end
```

Expand All @@ -670,7 +693,7 @@ client.post("http://httpbin.org/post", form: {name: "foo"})

#### Write a interceptor

Halite features has a killer feature is the **interceptor*, Use `Halite::Interceptor::Chain` to process with two result:
Halite features has a killer feature is the **interceptor*, Use `Halite::Feature::Chain` to process with two result:

- `next`: perform and run next interceptor
- `return`: perform and return
Expand All @@ -685,7 +708,7 @@ class AlwaysNotFound < Halite::Feature
chain.next(response)
end

Halite::Features.register "404", self
Halite.register_feature "404", self
end

class PoweredBy < Halite::Feature
Expand All @@ -698,7 +721,7 @@ class PoweredBy < Halite::Feature
end
end

Halite::Features.register "powered_by", self
Halite.register_feature "powered_by", self
end

r = Halite.use("404").use("powered_by").get("http://httpbin.org/user-agent")
Expand Down
16 changes: 16 additions & 0 deletions spec/halite/ext/namedtuple_to_h.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
struct NamedTuple
# Returns a `Hash` with the keys and values in this named tuple.
#
# TODO: This is fix bug, It will remove if PR is merged https://github.com/crystal-lang/crystal/pull/6628
def to_h
raise "Can't convert an empty NamedTuple to a Hash" if empty?

{% if T.size > 0 %}
{
{% for key in T %}
{{key.symbolize}} => self[{{key.symbolize}}],
{% end %}
}
{% end %}
end
end
10 changes: 10 additions & 0 deletions spec/halite/feature_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
require "../spec_helper"

describe Halite::Feature do
it "should a empty feature" do
feature = TestFeatures::Null.new
feature.responds_to?(:request).should be_true
feature.responds_to?(:response).should be_true
feature.responds_to?(:intercept).should be_true
end
end
38 changes: 0 additions & 38 deletions spec/halite/features/logger_spec.cr

This file was deleted.

35 changes: 35 additions & 0 deletions spec/halite/features/logging_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
require "../../spec_helper"

private class NulleLogger < Halite::Logging::Abstract
def request(request)
end

def response(response)
end
end

describe Halite::Logging do
it "should register a format" do
Halite::Logging.register "null", NulleLogger
Halite::Logging.availables.includes?("null").should be_true
Halite::Logging["null"].should eq(NulleLogger)
end

it "should use common as default logger" do
logger = Halite::Logging.new
logger.writer.should be_a(Halite::Logging::Common)
logger.writer.skip_request_body.should be_false
logger.writer.skip_response_body.should be_false
logger.writer.skip_benchmark.should be_false
logger.writer.colorize.should be_true
end

it "should use custom logger" do
logger = Halite::Logging.new(logger: NulleLogger.new)
logger.writer.should be_a(NulleLogger)
logger.writer.skip_request_body.should be_false
logger.writer.skip_response_body.should be_false
logger.writer.skip_benchmark.should be_false
logger.writer.colorize.should be_true
end
end
21 changes: 0 additions & 21 deletions spec/halite/features_spec.cr

This file was deleted.

28 changes: 7 additions & 21 deletions spec/halite/mime_type_spec.cr
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
require "../spec_helper"
require "yaml"

private class YAMLAdapter < Halite::MimeTypes::Adapter
private class YAMLAdapter < Halite::MimeType::Adapter
def decode(string)
YAML.parse string
end
Expand All @@ -11,28 +11,14 @@ private class YAMLAdapter < Halite::MimeTypes::Adapter
end
end

describe Halite::MimeTypes do
describe Halite::MimeType do
it "should register an adapter" do
Halite::MimeTypes["yaml"]?.should be_nil
Halite::MimeTypes["yml"]?.should be_nil
Halite::MimeType["yaml"]?.should be_nil
Halite::MimeType["yml"]?.should be_nil

Halite::MimeTypes.register_adapter "application/x-yaml", YAMLAdapter.new
Halite::MimeTypes.register_alias "application/x-yaml", "yaml"
Halite::MimeTypes.register_alias "application/x-yaml", "yml"
Halite::MimeType.register YAMLAdapter.new, "application/x-yaml", "yaml", "yml"

Halite::MimeTypes["yaml"].should be_a YAMLAdapter
Halite::MimeTypes["yml"].should be_a YAMLAdapter
end

it "should overwrite exists adapter" do
Halite::MimeTypes.register_adapter "application/json", YAMLAdapter.new
Halite::MimeTypes.register_alias "application/json", "json"

Halite::MimeTypes["json"].should be_a YAMLAdapter
Halite::MimeTypes["json"].should_not be_a Halite::MimeTypes::JSON

# Restore back for other specs
Halite::MimeTypes.register_adapter "application/json", Halite::MimeTypes::JSON.new
Halite::MimeTypes.register_alias "application/json", "json"
Halite::MimeType["yaml"].should be_a YAMLAdapter
Halite::MimeType["yml"].should be_a YAMLAdapter
end
end
6 changes: 3 additions & 3 deletions spec/halite/mime_types/json_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@ require "../../spec_helper"
private class Foo
end

describe Halite::MimeTypes::JSON do
describe Halite::MimeType::JSON do
describe "#encode" do
it "shoulds works with to_json class" do
json = Halite::MimeTypes::JSON.new
json = Halite::MimeType::JSON.new
json.encode({name: "foo"}).should eq(%Q{{"name":"foo"}})
end
end

describe "#decode" do
it "shoulds works with json string" do
json = Halite::MimeTypes::JSON.new
json = Halite::MimeType::JSON.new
json.decode(%Q{{"name": "foo"}}).should be_a(JSON::Any)
json.decode(%Q{{"name": "foo"}}).should eq({"name" => "foo"})
end
Expand Down
Loading