x 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 017d3f20bd339939e92fd91d8e1034ded6b3165c31734eb6b7b092e17cafb15a
4
- data.tar.gz: 8d4bf8295848cba641b9bc9ab6583eee426dcaed0eccfbedcfa982aa82423808
3
+ metadata.gz: 5aaa181ca277a20cc6ddb1ec8d4219535357611891da51642471f54c032a2b47
4
+ data.tar.gz: 43f3949ff58bb6e3248adad99324f25f6709ad507fc96bb75ff47eba832bddba
5
5
  SHA512:
6
- metadata.gz: '0597ac0dcb4633c682c237e24d701e8972ff5f7390feab5fa16d320654ecbdbe97ec356ebc6aaa11861d54267fe7ea14f89b43b5290c58202a0a65318940a268'
7
- data.tar.gz: f48b37b5042dd349bd822f1d8c2c4e27574e48494e1e8610f265e95eb608b33adee081b57c9b7f5234e7e799c6820aebc77e4f931e99feb80a3e2d77ffb8e523
6
+ metadata.gz: 612f01c108c9c219363f2910fe393795881151b008c6dc452210ead69c02d1421141061575e8d92f5108fbdc18a3949bc6c3330c9c73444495ed37b4df587e03
7
+ data.tar.gz: 1a5759aa9862066f735632e0ff63defeb6827bd63657013b88dd33d97792ca177f95859784f08ac32f0f31249ee6a26bdfab582218aee1abd79fcac71281b067
data/CHANGELOG.md CHANGED
@@ -1,3 +1,6 @@
1
+ ## [0.19.0] - 2026-03-01
2
+ * Add streaming support for filtered stream and volume stream endpoints
3
+
1
4
  ## [0.18.0] - 2026-01-06
2
5
  * Add OAuth 2.0 authentication with token refresh support (d4c03cb)
3
6
  * Add AccountUploader for profile image and banner uploads (7833dd2)
data/README.md CHANGED
@@ -71,11 +71,23 @@ ads_client = X::Client.new(base_url: "https://ads-api.twitter.com/12/", **x_cred
71
71
  ads_client.get("accounts")
72
72
  ```
73
73
 
74
+ ### Streaming
75
+
76
+ ```ruby
77
+ # Set up rules for filtered stream
78
+ x_client.post("tweets/search/stream/rules", '{"add": [{"value": "ruby"}]}')
79
+
80
+ # Stream matching posts in real time
81
+ x_client.stream("tweets/search/stream") do |tweet|
82
+ puts tweet["data"]["text"]
83
+ end
84
+ ```
85
+
74
86
  See other common usage [examples](https://github.com/sferik/x-ruby/tree/main/examples).
75
87
 
76
88
  ## History and Philosophy
77
89
 
78
- This library is a rewrite of the [Twitter Ruby library](https://github.com/sferik/twitter). Over 16 years of development, that library ballooned to over 3,000 lines of code (plus 7,500 lines of tests), not counting dependencies. This library is about 750 lines of code (plus 1335 test lines) and has no runtime dependencies. That doesn’t mean new features won’t be added over time, but the benefits of more code must be weighed against the benefits of less:
90
+ This library is a rewrite of the [Twitter Ruby library](https://github.com/sferik/twitter). Over 16 years of development, that library ballooned to over 3,000 lines of code (plus 7,500 lines of tests), not counting dependencies. This library is less than 1,000 lines of code (plus 2,000 test lines) and has no runtime dependencies. That doesn’t mean new features won’t be added over time, but the benefits of more code must be weighed against the benefits of less:
79
91
 
80
92
  * Less code is easier to maintain.
81
93
  * Less code means fewer bugs.
@@ -91,7 +103,7 @@ This code is not littered with comments that are intended to generate documentat
91
103
 
92
104
  ## Features
93
105
 
94
- If this entire library is implemented in just 750 lines of code, why should you use it at all vs. writing your own library that suits your needs? If you feel inspired to do that, don’t let me discourage you, but this library has some advanced features that may not be apparent without diving into the code:
106
+ If this entire library is implemented in under 1,000 lines of code, why should you use it at all vs. writing your own library that suits your needs? If you feel inspired to do that, don’t let me discourage you, but this library has some advanced features that may not be apparent without diving into the code:
95
107
 
96
108
  * OAuth 1.0 Revision A
97
109
  * OAuth 2.0
@@ -102,6 +114,7 @@ If this entire library is implemented in just 750 lines of code, why should you
102
114
  * HTTP timeout configuration
103
115
  * HTTP error handling
104
116
  * Rate limit handling
117
+ * Streaming (filtered stream, volume stream)
105
118
  * Parsing JSON into custom response objects (e.g. OpenStruct)
106
119
  * Configurable base URLs for accessing different APIs/versions
107
120
  * Parallel uploading of large media files in chunks
data/lib/x/client.rb CHANGED
@@ -8,6 +8,7 @@ require_relative "oauth2_authenticator"
8
8
  require_relative "redirect_handler"
9
9
  require_relative "request_builder"
10
10
  require_relative "response_parser"
11
+ require_relative "stream_parser"
11
12
 
12
13
  module X
13
14
  # A client for interacting with the X API
@@ -87,7 +88,7 @@ module X
87
88
  open_timeout: Connection::DEFAULT_OPEN_TIMEOUT,
88
89
  read_timeout: Connection::DEFAULT_READ_TIMEOUT,
89
90
  write_timeout: Connection::DEFAULT_WRITE_TIMEOUT,
90
- debug_output: Connection::DEFAULT_DEBUG_OUTPUT,
91
+ debug_output: nil,
91
92
  proxy_url: nil,
92
93
  default_array_class: DEFAULT_ARRAY_CLASS,
93
94
  default_object_class: DEFAULT_OBJECT_CLASS,
@@ -96,12 +97,12 @@ module X
96
97
  client_id:, client_secret:, refresh_token:)
97
98
  initialize_authenticator
98
99
  @base_url = base_url
99
- @default_array_class = default_array_class
100
- @default_object_class = default_object_class
100
+ initialize_default_classes(default_array_class:, default_object_class:)
101
101
  @connection = Connection.new(open_timeout:, read_timeout:, write_timeout:, debug_output:, proxy_url:)
102
102
  @request_builder = RequestBuilder.new
103
103
  @redirect_handler = RedirectHandler.new(connection: @connection, request_builder: @request_builder, max_redirects:)
104
104
  @response_parser = ResponseParser.new
105
+ @stream_parser = StreamParser.new
105
106
  end
106
107
 
107
108
  # Perform a GET request to the X API
@@ -144,8 +145,38 @@ module X
144
145
  execute_request(:delete, endpoint, headers:, array_class:, object_class:)
145
146
  end
146
147
 
148
+ # Stream data from the X API
149
+ #
150
+ # @api public
151
+ # @param endpoint [String] the streaming API endpoint
152
+ # @param headers [Hash] additional headers for the request
153
+ # @param array_class [Class] the class for parsing JSON arrays
154
+ # @param object_class [Class] the class for parsing JSON objects
155
+ # @yield [Hash, Array] each parsed JSON object from the stream
156
+ # @return [void]
157
+ # @raise [HTTPError] if the response is not successful
158
+ # @example Stream filtered tweets
159
+ # client.stream("tweets/search/stream") { |tweet| puts tweet }
160
+ def stream(endpoint, headers: {}, array_class: default_array_class, object_class: default_object_class, &block)
161
+ uri = URI.join(base_url, endpoint)
162
+ request = @request_builder.build(http_method: :get, uri:, headers:, authenticator:)
163
+ @connection.perform_stream(request:) do |response|
164
+ @stream_parser.process(response:, response_parser: @response_parser, array_class:, object_class:, &block)
165
+ end
166
+ end
167
+
147
168
  private
148
169
 
170
+ # Initialize default JSON parsing classes
171
+ # @api private
172
+ # @param default_array_class [Class] the default class for parsing JSON arrays
173
+ # @param default_object_class [Class] the default class for parsing JSON objects
174
+ # @return [void]
175
+ def initialize_default_classes(default_array_class:, default_object_class:)
176
+ @default_array_class = default_array_class
177
+ @default_object_class = default_object_class
178
+ end
179
+
149
180
  # Execute an HTTP request to the X API
150
181
  # @api private
151
182
  # @return [Hash, Array, nil] the parsed response body
data/lib/x/connection.rb CHANGED
@@ -20,8 +20,6 @@ module X
20
20
  DEFAULT_READ_TIMEOUT = 60 # seconds
21
21
  # Default timeout for writing requests in seconds
22
22
  DEFAULT_WRITE_TIMEOUT = 60 # seconds
23
- # Default debug output destination
24
- DEFAULT_DEBUG_OUTPUT = File.open(IO::NULL, "w")
25
23
  # Network errors that should be wrapped in NetworkError
26
24
  NETWORK_ERRORS = [
27
25
  Errno::ECONNREFUSED,
@@ -92,7 +90,7 @@ module X
92
90
  # @example Create a connection with custom timeouts
93
91
  # connection = X::Connection.new(open_timeout: 30, read_timeout: 30)
94
92
  def initialize(open_timeout: DEFAULT_OPEN_TIMEOUT, read_timeout: DEFAULT_READ_TIMEOUT,
95
- write_timeout: DEFAULT_WRITE_TIMEOUT, debug_output: DEFAULT_DEBUG_OUTPUT, proxy_url: nil)
93
+ write_timeout: DEFAULT_WRITE_TIMEOUT, debug_output: nil, proxy_url: nil)
96
94
  @open_timeout = open_timeout
97
95
  @read_timeout = read_timeout
98
96
  @write_timeout = write_timeout
@@ -118,6 +116,25 @@ module X
118
116
  raise NetworkError, "Network error: #{e}"
119
117
  end
120
118
 
119
+ # Perform a streaming HTTP request
120
+ #
121
+ # @api public
122
+ # @param request [Net::HTTPRequest] the HTTP request to perform
123
+ # @yield [Net::HTTPResponse] the HTTP response for streaming
124
+ # @return [void]
125
+ # @raise [NetworkError] if a network error occurs
126
+ # @example Perform a streaming request
127
+ # connection.perform_stream(request: request) { |response| response.read_body { |chunk| } }
128
+ def perform_stream(request:, &)
129
+ host = request.uri.host || DEFAULT_HOST
130
+ port = request.uri.port || DEFAULT_PORT
131
+ http_client = build_http_client(host, port)
132
+ http_client.use_ssl = request.uri.scheme.eql?("https")
133
+ http_client.request(request, &)
134
+ rescue *NETWORK_ERRORS => e
135
+ raise NetworkError, "Network error: #{e}"
136
+ end
137
+
121
138
  # Set the proxy URL for requests
122
139
  #
123
140
  # @api public
@@ -0,0 +1,75 @@
1
+ require "json"
2
+ require "net/http"
3
+ require_relative "response_parser"
4
+
5
+ module X
6
+ # Handles streaming responses from the X API
7
+ # @api public
8
+ class StreamParser
9
+ # Line delimiter for streaming responses
10
+ LINE_DELIMITER = "\r\n".freeze
11
+
12
+ # Process a streaming response and yield parsed JSON objects
13
+ #
14
+ # @api public
15
+ # @param response [Net::HTTPResponse] the HTTP response to stream
16
+ # @param response_parser [ResponseParser] the response parser for error handling
17
+ # @param array_class [Class, nil] the class for parsing JSON arrays
18
+ # @param object_class [Class, nil] the class for parsing JSON objects
19
+ # @yield [Hash, Array] each parsed JSON object from the stream
20
+ # @return [void]
21
+ # @raise [HTTPError] if the response is not successful
22
+ # @example Process a streaming response
23
+ # handler.process(response: response, response_parser: parser) { |json| puts json }
24
+ def process(response:, response_parser:, array_class: nil, object_class: nil, &block)
25
+ response_parser.parse(response:) unless response.is_a?(Net::HTTPSuccess)
26
+
27
+ buffer = +""
28
+ response.read_body do |chunk|
29
+ buffer << chunk
30
+ process_buffer(buffer:, array_class:, object_class:, &block)
31
+ end
32
+ process_remaining(buffer:, array_class:, object_class:, &block)
33
+ end
34
+
35
+ private
36
+
37
+ # Process complete lines from the buffer
38
+ # @api private
39
+ # @param buffer [String] the accumulated data buffer
40
+ # @param array_class [Class, nil] the class for parsing JSON arrays
41
+ # @param object_class [Class, nil] the class for parsing JSON objects
42
+ # @yield [Hash, Array] each parsed JSON object
43
+ # @return [void]
44
+ def process_buffer(buffer:, array_class:, object_class:, &)
45
+ while (line_end = buffer.index(LINE_DELIMITER))
46
+ line = buffer.slice!(0, line_end) # : String
47
+ buffer.delete_prefix!(LINE_DELIMITER)
48
+ yield_json(line:, array_class:, object_class:, &) unless line.empty?
49
+ end
50
+ end
51
+
52
+ # Process any remaining data after the stream ends
53
+ # @api private
54
+ # @param buffer [String] the remaining data buffer
55
+ # @param array_class [Class, nil] the class for parsing JSON arrays
56
+ # @param object_class [Class, nil] the class for parsing JSON objects
57
+ # @yield [Hash, Array] the parsed JSON object
58
+ # @return [void]
59
+ def process_remaining(buffer:, array_class:, object_class:, &)
60
+ buffer.strip!
61
+ yield_json(line: buffer, array_class:, object_class:, &) unless buffer.empty?
62
+ end
63
+
64
+ # Parse a line as JSON and yield the result
65
+ # @api private
66
+ # @param line [String] the JSON line to parse
67
+ # @param array_class [Class, nil] the class for parsing JSON arrays
68
+ # @param object_class [Class, nil] the class for parsing JSON objects
69
+ # @yield [Hash, Array] the parsed JSON object
70
+ # @return [void]
71
+ def yield_json(line:, array_class:, object_class:)
72
+ yield JSON.parse(line, array_class:, object_class:)
73
+ end
74
+ end
75
+ end
data/lib/x/version.rb CHANGED
@@ -2,5 +2,5 @@ require "rubygems/version"
2
2
 
3
3
  module X
4
4
  # The current version of the X gem
5
- VERSION = Gem::Version.create("0.18.0")
5
+ VERSION = Gem::Version.create("0.19.0")
6
6
  end
data/sig/x.rbs CHANGED
@@ -125,7 +125,7 @@ module X
125
125
  DEFAULT_OPEN_TIMEOUT: Integer
126
126
  DEFAULT_READ_TIMEOUT: Integer
127
127
  DEFAULT_WRITE_TIMEOUT: Integer
128
- DEFAULT_DEBUG_OUTPUT: IO
128
+
129
129
  NETWORK_ERRORS: Array[(singleton(Errno::ECONNREFUSED) | singleton(Errno::ECONNRESET) | singleton(Net::OpenTimeout) | singleton(Net::ReadTimeout) | singleton(OpenSSL::SSL::SSLError))]
130
130
 
131
131
  @proxy_url: URI::Generic | String
@@ -146,6 +146,7 @@ module X
146
146
  def initialize: (?open_timeout: Float | Integer, ?read_timeout: Float | Integer, ?write_timeout: Float | Integer, ?proxy_url: URI::Generic? | String?, ?debug_output: IO) -> void
147
147
  def proxy_url=: (URI::Generic | String proxy_url) -> void
148
148
  def perform: (request: Net::HTTPRequest) -> Net::HTTPResponse
149
+ def perform_stream: (request: Net::HTTPRequest) { (Net::HTTPResponse) -> void } -> void
149
150
 
150
151
  private
151
152
  def build_http_client: (?String host, ?Integer port) -> Net::HTTP
@@ -208,6 +209,17 @@ module X
208
209
  def json?: (Net::HTTPResponse response) -> bool
209
210
  end
210
211
 
212
+ class StreamParser
213
+ LINE_DELIMITER: String
214
+
215
+ def process: (response: Net::HTTPResponse, response_parser: ResponseParser, ?array_class: Class?, ?object_class: Class?) { (untyped) -> void } -> void
216
+
217
+ private
218
+ def process_buffer: (buffer: String, array_class: Class?, object_class: Class?) { (untyped) -> void } -> void
219
+ def process_remaining: (buffer: String, array_class: Class?, object_class: Class?) { (untyped) -> void } -> void
220
+ def yield_json: (line: String, array_class: Class?, object_class: Class?) { (untyped) -> void } -> void
221
+ end
222
+
211
223
  class OAuth2Authenticator < Authenticator
212
224
  TOKEN_PATH: String
213
225
  TOKEN_HOST: String
@@ -270,6 +282,7 @@ module X
270
282
  @request_builder: RequestBuilder
271
283
  @redirect_handler: RedirectHandler
272
284
  @response_parser: ResponseParser
285
+ @stream_parser: StreamParser
273
286
 
274
287
  attr_accessor base_url: String
275
288
  attr_accessor default_array_class: singleton(Array)
@@ -280,8 +293,10 @@ module X
280
293
  def post: (String endpoint, ?String? body, ?headers: Hash[String, String], ?array_class: Class, ?object_class: Class) -> untyped
281
294
  def put: (String endpoint, ?String? body, ?headers: Hash[String, String], ?array_class: Class, ?object_class: Class) -> untyped
282
295
  def delete: (String endpoint, ?headers: Hash[String, String], ?array_class: Class, ?object_class: Class) -> untyped
296
+ def stream: (String endpoint, ?headers: Hash[String, String], ?array_class: Class, ?object_class: Class) { (untyped) -> void } -> void
283
297
 
284
298
  private
299
+ def initialize_default_classes: (default_array_class: singleton(Array), default_object_class: singleton(Hash)) -> void
285
300
  def execute_request: (:delete | :get | :post | :put http_method, String endpoint, ?body: String?, ?headers: Hash[String, String], ?array_class: Class, ?object_class: Class) -> nil
286
301
  end
287
302
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: x
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.18.0
4
+ version: 0.19.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Erik Berlin
@@ -70,6 +70,7 @@ files:
70
70
  - lib/x/redirect_handler.rb
71
71
  - lib/x/request_builder.rb
72
72
  - lib/x/response_parser.rb
73
+ - lib/x/stream_parser.rb
73
74
  - lib/x/version.rb
74
75
  - sig/x.rbs
75
76
  homepage: https://sferik.github.io/x-ruby
@@ -98,7 +99,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
98
99
  - !ruby/object:Gem::Version
99
100
  version: '0'
100
101
  requirements: []
101
- rubygems_version: 4.0.3
102
+ rubygems_version: 4.0.7
102
103
  specification_version: 4
103
104
  summary: A Ruby interface to the X API.
104
105
  test_files: []