Skip to content

Commit 698c6ad

Browse files
committedSep 8, 2023
Initial commit
0 parents  commit 698c6ad

26 files changed

+593
-0
lines changed
 

‎.editorconfig

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
root = true
2+
3+
[*.cr]
4+
charset = utf-8
5+
end_of_line = lf
6+
insert_final_newline = true
7+
indent_style = space
8+
indent_size = 2
9+
trim_trailing_whitespace = true

‎.gitignore

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/docs/
2+
/lib/
3+
/bin/
4+
/.shards/
5+
*.dwarf
6+
7+
# Libraries don't need dependency lock
8+
# Dependencies will be locked in applications that use them
9+
/shard.lock

‎LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2023 Erik Berlin <sferik@gmail.com>
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

‎README.md

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# X
2+
3+
#### A Crystal interface to the X API.
4+
5+
## Installation
6+
7+
Add the dependency to your `shard.yml`:
8+
9+
```yaml
10+
dependencies:
11+
x:
12+
github: sferik/x-crystal
13+
```
14+
15+
Then run:
16+
17+
shards install
18+
19+
## Usage
20+
21+
```crystal
22+
require "x"
23+
24+
x_credentials = {
25+
api_key: "INSERT YOUR X API KEY HERE",
26+
api_key_secret: "INSERT YOUR X API KEY SECRET HERE",
27+
access_token: "INSERT YOUR X ACCESS TOKEN HERE",
28+
access_token_secret: "INSERT YOUR X ACCESS TOKEN SECRET HERE",
29+
}
30+
31+
# Initialize an X API client with your OAuth credentials
32+
x_client = X::Client.new(**x_credentials)
33+
34+
# Get data about yourself
35+
x_client.get("users/me")
36+
# {"data"=>{"id"=>"7505382", "name"=>"Erik Berlin", "username"=>"sferik"}}
37+
38+
# Post a tweet
39+
tweet = x_client.post("tweets", '{"text":"Hello, World! (from @gem)"}')
40+
# {"data"=>{"edit_history_tweet_ids"=>["1234567890123456789"], "id"=>"1234567890123456789", "text"=>"Hello, World! (from @gem)"}}
41+
42+
# Delete the tweet you just posted
43+
x_client.delete("tweets/#{tweet["data"]["id"]}")
44+
# {"data"=>{"deleted"=>true}}
45+
46+
# Initialize an API v1.1 client
47+
v1_client = X::Client.new(base_url: "https://api.twitter.com/1.1/", **x_credentials)
48+
49+
# Request your account settings
50+
v1_client.get("account/settings.json")
51+
52+
# Initialize an X Ads API client
53+
ads_client = X::Client.new(base_url: "https://ads-api.twitter.com/12/", **x_credentials)
54+
55+
# Request your ad accounts
56+
ads_client.get("accounts")
57+
```
58+
59+
## History and Philosophy
60+
61+
This library is a port of the [X Ruby library](https://github.com/sferik/x-ruby), which is a rewrite of the [Twitter Ruby library](https://github.com/sferik/twitter). The Ruby and Crystal version have the same basic interface and design philosophy, but this is not a strict guarantee.
62+
63+
## Development
64+
65+
1. Checkout and repo:
66+
67+
git checkout git@github.com:sferik/x-crystal.git
68+
69+
2. Enter the repo’s directory:
70+
71+
cd x-crystal
72+
73+
3. Install dependencies via Shards:
74+
75+
shards install
76+
77+
4. Ensure all tests pass:
78+
79+
crystal spec
80+
81+
5. Create a new branch for your feature or bug fix:
82+
83+
git checkout -b my-new-feature
84+
85+
6. Write your feature or bug fix
86+
87+
7. Ensure the code is formatted properly:
88+
89+
crystal tool format
90+
91+
8. Ensure the tests still pass:
92+
93+
crystal spec
94+
95+
9. Write tests to cover your new feature and ensure they pass:
96+
97+
crystal spec
98+
99+
10. Commit your changes:
100+
101+
git commit -am "Add some feature"
102+
103+
11. Push to the branch:
104+
105+
git push origin my-new-feature
106+
107+
12. Open a Pull Request on GitHub
108+
109+
## Contributors
110+
111+
- [Erik Berlin](https://github.com/sferik) - creator and maintainer
112+
113+
## License
114+
115+
The code is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).

‎shard.yml

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
name: x
2+
version: 0.1.0
3+
4+
authors:
5+
- Erik Berlin <sferik@gmail.com>
6+
7+
crystal: 1.9.2
8+
9+
development_dependencies:
10+
webmock:
11+
github: manastech/webmock.cr
12+
branch: master
13+
14+
license: MIT

‎spec/client_spec.cr

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
require "./spec_helper"
2+
3+
module X
4+
describe Client do
5+
client = Client.new(api_key: "api_key", api_key_secret: "api_key_secret", access_token: "access_token", access_token_secret: "access_token_secret")
6+
7+
describe "#get" do
8+
it "sends a GET request" do
9+
endpoint = "test_endpoint"
10+
stub_request(:get, endpoint, 200)
11+
response = client.get(endpoint)
12+
response.should eq({} of String => String)
13+
end
14+
end
15+
16+
describe "#post" do
17+
it "sends a POST request with body" do
18+
endpoint = "test_endpoint"
19+
body = "{\"data\":\"test\"}"
20+
stub_request(:post, endpoint, 200)
21+
response = client.post(endpoint, body)
22+
response.should eq({} of String => String)
23+
end
24+
end
25+
26+
describe "#put" do
27+
it "sends a PUT request with body" do
28+
endpoint = "test_endpoint"
29+
body = "{\"data\":\"test\"}"
30+
stub_request(:put, endpoint, 200)
31+
response = client.put(endpoint, body)
32+
response.should eq({} of String => String)
33+
end
34+
end
35+
36+
describe "#delete" do
37+
it "sends a DELETE request" do
38+
endpoint = "test_endpoint"
39+
stub_request(:delete, endpoint, 200)
40+
response = client.delete(endpoint)
41+
response.should eq({} of String => String)
42+
end
43+
end
44+
end
45+
end

‎spec/response_handler_spec.cr

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
require "./spec_helper"
2+
3+
module X
4+
describe ResponseHandler do
5+
it "handles a successful response" do
6+
response = HTTP::Client::Response.new(200, "{\"status\":\"success\"}")
7+
handler = ResponseHandler.new
8+
result = handler.handle(response)
9+
result.should eq({"status" => "success"})
10+
end
11+
12+
it "raises a BadRequestError for a 400 response" do
13+
response = HTTP::Client::Response.new(400, "Bad Request")
14+
handler = ResponseHandler.new
15+
expect_raises BadRequestError, "400 Bad Request - Bad Request" do
16+
handler.handle(response)
17+
end
18+
end
19+
20+
it "raises a AuthenticationError for a 401 response" do
21+
response = HTTP::Client::Response.new(401, "Unauthorized")
22+
handler = ResponseHandler.new
23+
expect_raises AuthenticationError, "401 Unauthorized - Unauthorized" do
24+
handler.handle(response)
25+
end
26+
end
27+
28+
it "raises a ForbiddenError for a 403 response" do
29+
response = HTTP::Client::Response.new(403, "Forbidden")
30+
handler = ResponseHandler.new
31+
expect_raises ForbiddenError, "403 Forbidden - Forbidden" do
32+
handler.handle(response)
33+
end
34+
end
35+
36+
it "raises a NotFoundError for a 404 response" do
37+
response = HTTP::Client::Response.new(404, "Not Found")
38+
handler = ResponseHandler.new
39+
expect_raises NotFoundError, "404 Not Found - Not Found" do
40+
handler.handle(response)
41+
end
42+
end
43+
44+
it "raises a TooManyRequestsError for a 429 response" do
45+
response = HTTP::Client::Response.new(429, "Too Many Requests")
46+
handler = ResponseHandler.new
47+
expect_raises TooManyRequestsError, "429 Too Many Requests - Too Many Requests" do
48+
handler.handle(response)
49+
end
50+
end
51+
52+
it "raises a ClientError for a 499 response" do
53+
response = HTTP::Client::Response.new(499, "Client Error")
54+
handler = ResponseHandler.new
55+
expect_raises ClientError, "499 Client Error - Client Error" do
56+
handler.handle(response)
57+
end
58+
end
59+
60+
it "raises a ServerError for a 500 response" do
61+
response = HTTP::Client::Response.new(500, "Internal Server Error")
62+
handler = ResponseHandler.new
63+
expect_raises ServerError, "500 Server Error - Internal Server Error" do
64+
handler.handle(response)
65+
end
66+
end
67+
68+
it "raises a ServerError for a 503 response" do
69+
response = HTTP::Client::Response.new(503, "Service Unavailable")
70+
handler = ResponseHandler.new
71+
expect_raises ServiceUnavailableError, "503 Service Unavailable - Service Unavailable" do
72+
handler.handle(response)
73+
end
74+
end
75+
76+
it "raises an Error for an unknown response" do
77+
response = HTTP::Client::Response.new(600, "Unknown")
78+
handler = ResponseHandler.new
79+
expect_raises Error, "600 Unknown Response - Unknown" do
80+
handler.handle(response)
81+
end
82+
end
83+
end
84+
end

‎spec/spec_helper.cr

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
require "../src/x"
2+
require "spec"
3+
require "webmock"
4+
5+
module SpecHelper
6+
def stub_request(http_method : Symbol, endpoint : String, status : Int32, headers : HTTP::Headers? = nil, body : String? = "{}")
7+
full_url = "#{X::ClientDefaults::DEFAULT_BASE_URL}#{endpoint}"
8+
WebMock.stub(http_method, full_url).to_return(status: status, body: body, headers: headers)
9+
end
10+
end
11+
12+
Spec.before_each do
13+
WebMock.reset
14+
end
15+
16+
include SpecHelper

‎spec/version_spec.cr

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
require "./spec_helper"
2+
3+
module X
4+
describe VERSION do
5+
it "has a version" do
6+
X::VERSION.should_not be_nil
7+
end
8+
9+
it "has a major version" do
10+
X::VERSION.major.should_not be_nil
11+
end
12+
13+
it "has a minor version" do
14+
X::VERSION.minor.should_not be_nil
15+
end
16+
17+
it "has a patch version" do
18+
X::VERSION.patch.should_not be_nil
19+
end
20+
21+
it "converts to a string" do
22+
X::VERSION.to_s.should be_a(String)
23+
end
24+
end
25+
end

‎src/x.cr

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
require "./x/client"

‎src/x/authenticator.cr

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
require "oauth"
2+
require "uri"
3+
require "./client_defaults"
4+
5+
module X
6+
class Authenticator
7+
include ClientDefaults
8+
9+
getter! api_key : String
10+
getter! api_key_secret : String
11+
getter! access_token : String
12+
getter! access_token_secret : String
13+
getter! oauth_access_token : OAuth::AccessToken
14+
getter! oauth_consumer : OAuth::Consumer
15+
16+
def initialize(base_url : URI, @api_key : String, @api_key_secret : String, @access_token : String, @access_token_secret : String)
17+
host = base_url.host || DEFAULT_HOST
18+
@oauth_consumer = OAuth::Consumer.new(host, api_key, api_key_secret)
19+
@oauth_access_token = OAuth::AccessToken.new(access_token, access_token_secret)
20+
end
21+
22+
def authenticate(http_client : HTTP::Client)
23+
oauth_consumer.authenticate(http_client, oauth_access_token)
24+
end
25+
end
26+
end

0 commit comments

Comments
 (0)
Please sign in to comment.