spikard 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE +1 -1
- data/README.md +659 -659
- data/ext/spikard_rb/Cargo.toml +17 -17
- data/ext/spikard_rb/extconf.rb +10 -10
- data/ext/spikard_rb/src/lib.rs +6 -6
- data/lib/spikard/app.rb +386 -386
- data/lib/spikard/background.rb +27 -27
- data/lib/spikard/config.rb +396 -396
- data/lib/spikard/converters.rb +13 -13
- data/lib/spikard/handler_wrapper.rb +113 -113
- data/lib/spikard/provide.rb +214 -214
- data/lib/spikard/response.rb +173 -173
- data/lib/spikard/schema.rb +243 -243
- data/lib/spikard/sse.rb +111 -111
- data/lib/spikard/streaming_response.rb +44 -44
- data/lib/spikard/testing.rb +221 -221
- data/lib/spikard/upload_file.rb +131 -131
- data/lib/spikard/version.rb +5 -5
- data/lib/spikard/websocket.rb +59 -59
- data/lib/spikard.rb +43 -43
- data/sig/spikard.rbs +360 -360
- metadata +5 -8
- /data/vendor/bundle/ruby/{3.3.0 → 3.4.0}/gems/diff-lcs-1.6.2/mise.toml +0 -0
- /data/vendor/bundle/ruby/{3.3.0 → 3.4.0}/gems/rake-compiler-dock-1.10.0/build/buildkitd.toml +0 -0
data/lib/spikard/upload_file.rb
CHANGED
|
@@ -1,131 +1,131 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'stringio'
|
|
4
|
-
require 'base64'
|
|
5
|
-
|
|
6
|
-
module Spikard
|
|
7
|
-
# File upload handling for multipart/form-data requests
|
|
8
|
-
#
|
|
9
|
-
# This class provides an interface for handling file uploads,
|
|
10
|
-
# designed to be compatible with Rails patterns while optimized
|
|
11
|
-
# for Spikard's Rust-backed request processing.
|
|
12
|
-
#
|
|
13
|
-
# @example
|
|
14
|
-
# app.post('/upload') do |body|
|
|
15
|
-
# file = body[:file] # UploadFile instance
|
|
16
|
-
# content = file.read
|
|
17
|
-
# {
|
|
18
|
-
# filename: file.filename,
|
|
19
|
-
# size: file.size,
|
|
20
|
-
# content_type: file.content_type,
|
|
21
|
-
# description: body[:description]
|
|
22
|
-
# }
|
|
23
|
-
# end
|
|
24
|
-
class UploadFile
|
|
25
|
-
# @return [String] Original filename from the client
|
|
26
|
-
attr_reader :filename
|
|
27
|
-
|
|
28
|
-
# @return [String] MIME type of the uploaded file
|
|
29
|
-
attr_reader :content_type
|
|
30
|
-
|
|
31
|
-
# @return [Integer] Size of the file in bytes
|
|
32
|
-
attr_reader :size
|
|
33
|
-
|
|
34
|
-
# @return [Hash<String, String>] Additional headers associated with this file field
|
|
35
|
-
attr_reader :headers
|
|
36
|
-
|
|
37
|
-
# Create a new UploadFile instance
|
|
38
|
-
#
|
|
39
|
-
# @param filename [String] Original filename from the client
|
|
40
|
-
# @param content [String] File contents (may be base64 encoded)
|
|
41
|
-
# @param content_type [String, nil] MIME type (defaults to "application/octet-stream")
|
|
42
|
-
# @param size [Integer, nil] File size in bytes (computed from content if not provided)
|
|
43
|
-
# @param headers [Hash<String, String>, nil] Additional headers from the multipart field
|
|
44
|
-
# @param content_encoding [String, nil] Encoding type (e.g., "base64")
|
|
45
|
-
def initialize(filename, content, content_type: nil, size: nil, headers: nil, content_encoding: nil)
|
|
46
|
-
@filename = filename
|
|
47
|
-
@content_type = content_type || 'application/octet-stream'
|
|
48
|
-
@headers = headers || {}
|
|
49
|
-
|
|
50
|
-
# Decode content if base64 encoded
|
|
51
|
-
@content = if content_encoding == 'base64' || base64_encoded?(content)
|
|
52
|
-
Base64.decode64(content)
|
|
53
|
-
else
|
|
54
|
-
content
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
@size = size || @content.bytesize
|
|
58
|
-
@io = StringIO.new(@content)
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
# Read file contents
|
|
62
|
-
#
|
|
63
|
-
# @param size [Integer, nil] Number of bytes to read (nil for all remaining)
|
|
64
|
-
# @return [String] File contents
|
|
65
|
-
def read(size = nil)
|
|
66
|
-
@io.read(size)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# Read file contents as text
|
|
70
|
-
#
|
|
71
|
-
# @param encoding [String] Character encoding (defaults to UTF-8)
|
|
72
|
-
# @return [String] File contents as text
|
|
73
|
-
def text(encoding: 'UTF-8')
|
|
74
|
-
@content.force_encoding(encoding)
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
# Seek to a specific position in the file
|
|
78
|
-
#
|
|
79
|
-
# @param offset [Integer] Byte offset
|
|
80
|
-
# @param whence [Integer] Position reference (IO::SEEK_SET, IO::SEEK_CUR, IO::SEEK_END)
|
|
81
|
-
# @return [Integer] New position
|
|
82
|
-
def seek(offset, whence = IO::SEEK_SET)
|
|
83
|
-
@io.seek(offset, whence)
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
# Get current position in the file
|
|
87
|
-
#
|
|
88
|
-
# @return [Integer] Current byte offset
|
|
89
|
-
def tell
|
|
90
|
-
@io.tell
|
|
91
|
-
end
|
|
92
|
-
alias pos tell
|
|
93
|
-
|
|
94
|
-
# Rewind to the beginning of the file
|
|
95
|
-
#
|
|
96
|
-
# @return [Integer] Always returns 0
|
|
97
|
-
def rewind
|
|
98
|
-
@io.rewind
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
# Close the file (no-op for StringIO-based implementation)
|
|
102
|
-
#
|
|
103
|
-
# @return [nil]
|
|
104
|
-
def close
|
|
105
|
-
@io.close
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
# Check if file is closed
|
|
109
|
-
#
|
|
110
|
-
# @return [Boolean]
|
|
111
|
-
def closed?
|
|
112
|
-
@io.closed?
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
# Get the raw content as a string
|
|
116
|
-
#
|
|
117
|
-
# @return [String] Raw file content
|
|
118
|
-
attr_reader :content
|
|
119
|
-
|
|
120
|
-
private
|
|
121
|
-
|
|
122
|
-
# Check if a string appears to be base64 encoded
|
|
123
|
-
#
|
|
124
|
-
# @param str [String] String to check
|
|
125
|
-
# @return [Boolean]
|
|
126
|
-
def base64_encoded?(str)
|
|
127
|
-
# Simple heuristic: check if string matches base64 pattern
|
|
128
|
-
str.is_a?(String) && str.match?(%r{\A[A-Za-z0-9+/]*={0,2}\z})
|
|
129
|
-
end
|
|
130
|
-
end
|
|
131
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'stringio'
|
|
4
|
+
require 'base64'
|
|
5
|
+
|
|
6
|
+
module Spikard
|
|
7
|
+
# File upload handling for multipart/form-data requests
|
|
8
|
+
#
|
|
9
|
+
# This class provides an interface for handling file uploads,
|
|
10
|
+
# designed to be compatible with Rails patterns while optimized
|
|
11
|
+
# for Spikard's Rust-backed request processing.
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# app.post('/upload') do |body|
|
|
15
|
+
# file = body[:file] # UploadFile instance
|
|
16
|
+
# content = file.read
|
|
17
|
+
# {
|
|
18
|
+
# filename: file.filename,
|
|
19
|
+
# size: file.size,
|
|
20
|
+
# content_type: file.content_type,
|
|
21
|
+
# description: body[:description]
|
|
22
|
+
# }
|
|
23
|
+
# end
|
|
24
|
+
class UploadFile
|
|
25
|
+
# @return [String] Original filename from the client
|
|
26
|
+
attr_reader :filename
|
|
27
|
+
|
|
28
|
+
# @return [String] MIME type of the uploaded file
|
|
29
|
+
attr_reader :content_type
|
|
30
|
+
|
|
31
|
+
# @return [Integer] Size of the file in bytes
|
|
32
|
+
attr_reader :size
|
|
33
|
+
|
|
34
|
+
# @return [Hash<String, String>] Additional headers associated with this file field
|
|
35
|
+
attr_reader :headers
|
|
36
|
+
|
|
37
|
+
# Create a new UploadFile instance
|
|
38
|
+
#
|
|
39
|
+
# @param filename [String] Original filename from the client
|
|
40
|
+
# @param content [String] File contents (may be base64 encoded)
|
|
41
|
+
# @param content_type [String, nil] MIME type (defaults to "application/octet-stream")
|
|
42
|
+
# @param size [Integer, nil] File size in bytes (computed from content if not provided)
|
|
43
|
+
# @param headers [Hash<String, String>, nil] Additional headers from the multipart field
|
|
44
|
+
# @param content_encoding [String, nil] Encoding type (e.g., "base64")
|
|
45
|
+
def initialize(filename, content, content_type: nil, size: nil, headers: nil, content_encoding: nil)
|
|
46
|
+
@filename = filename
|
|
47
|
+
@content_type = content_type || 'application/octet-stream'
|
|
48
|
+
@headers = headers || {}
|
|
49
|
+
|
|
50
|
+
# Decode content if base64 encoded
|
|
51
|
+
@content = if content_encoding == 'base64' || base64_encoded?(content)
|
|
52
|
+
Base64.decode64(content)
|
|
53
|
+
else
|
|
54
|
+
content
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
@size = size || @content.bytesize
|
|
58
|
+
@io = StringIO.new(@content)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Read file contents
|
|
62
|
+
#
|
|
63
|
+
# @param size [Integer, nil] Number of bytes to read (nil for all remaining)
|
|
64
|
+
# @return [String] File contents
|
|
65
|
+
def read(size = nil)
|
|
66
|
+
@io.read(size)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Read file contents as text
|
|
70
|
+
#
|
|
71
|
+
# @param encoding [String] Character encoding (defaults to UTF-8)
|
|
72
|
+
# @return [String] File contents as text
|
|
73
|
+
def text(encoding: 'UTF-8')
|
|
74
|
+
@content.force_encoding(encoding)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Seek to a specific position in the file
|
|
78
|
+
#
|
|
79
|
+
# @param offset [Integer] Byte offset
|
|
80
|
+
# @param whence [Integer] Position reference (IO::SEEK_SET, IO::SEEK_CUR, IO::SEEK_END)
|
|
81
|
+
# @return [Integer] New position
|
|
82
|
+
def seek(offset, whence = IO::SEEK_SET)
|
|
83
|
+
@io.seek(offset, whence)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Get current position in the file
|
|
87
|
+
#
|
|
88
|
+
# @return [Integer] Current byte offset
|
|
89
|
+
def tell
|
|
90
|
+
@io.tell
|
|
91
|
+
end
|
|
92
|
+
alias pos tell
|
|
93
|
+
|
|
94
|
+
# Rewind to the beginning of the file
|
|
95
|
+
#
|
|
96
|
+
# @return [Integer] Always returns 0
|
|
97
|
+
def rewind
|
|
98
|
+
@io.rewind
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Close the file (no-op for StringIO-based implementation)
|
|
102
|
+
#
|
|
103
|
+
# @return [nil]
|
|
104
|
+
def close
|
|
105
|
+
@io.close
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Check if file is closed
|
|
109
|
+
#
|
|
110
|
+
# @return [Boolean]
|
|
111
|
+
def closed?
|
|
112
|
+
@io.closed?
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Get the raw content as a string
|
|
116
|
+
#
|
|
117
|
+
# @return [String] Raw file content
|
|
118
|
+
attr_reader :content
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
# Check if a string appears to be base64 encoded
|
|
123
|
+
#
|
|
124
|
+
# @param str [String] String to check
|
|
125
|
+
# @return [Boolean]
|
|
126
|
+
def base64_encoded?(str)
|
|
127
|
+
# Simple heuristic: check if string matches base64 pattern
|
|
128
|
+
str.is_a?(String) && str.match?(%r{\A[A-Za-z0-9+/]*={0,2}\z})
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
data/lib/spikard/version.rb
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Spikard
|
|
4
|
-
VERSION = '0.3.
|
|
5
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spikard
|
|
4
|
+
VERSION = '0.3.1'
|
|
5
|
+
end
|
data/lib/spikard/websocket.rb
CHANGED
|
@@ -1,59 +1,59 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Spikard
|
|
4
|
-
# Base class for WebSocket message handlers.
|
|
5
|
-
#
|
|
6
|
-
# Implement this class to handle WebSocket connections and messages.
|
|
7
|
-
#
|
|
8
|
-
# @example
|
|
9
|
-
# class ChatHandler < Spikard::WebSocketHandler
|
|
10
|
-
# def handle_message(message)
|
|
11
|
-
# # Echo message back
|
|
12
|
-
# message
|
|
13
|
-
# end
|
|
14
|
-
#
|
|
15
|
-
# def on_connect
|
|
16
|
-
# puts "Client connected"
|
|
17
|
-
# end
|
|
18
|
-
#
|
|
19
|
-
# def on_disconnect
|
|
20
|
-
# puts "Client disconnected"
|
|
21
|
-
# end
|
|
22
|
-
# end
|
|
23
|
-
#
|
|
24
|
-
# app = Spikard::App.new
|
|
25
|
-
#
|
|
26
|
-
# app.websocket('/chat') do
|
|
27
|
-
# ChatHandler.new
|
|
28
|
-
# end
|
|
29
|
-
#
|
|
30
|
-
# app.run
|
|
31
|
-
class WebSocketHandler
|
|
32
|
-
# Handle an incoming WebSocket message.
|
|
33
|
-
#
|
|
34
|
-
# @param message [Hash] Parsed JSON message from the client
|
|
35
|
-
# @return [Hash, nil] Optional response message to send back to the client.
|
|
36
|
-
# Return nil to not send a response.
|
|
37
|
-
def handle_message(message)
|
|
38
|
-
raise NotImplementedError, "#{self.class.name} must implement #handle_message"
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
# Called when a client connects.
|
|
42
|
-
#
|
|
43
|
-
# Override this method to perform initialization when a client connects.
|
|
44
|
-
#
|
|
45
|
-
# @return [void]
|
|
46
|
-
def on_connect
|
|
47
|
-
# Optional hook - default implementation does nothing
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# Called when a client disconnects.
|
|
51
|
-
#
|
|
52
|
-
# Override this method to perform cleanup when a client disconnects.
|
|
53
|
-
#
|
|
54
|
-
# @return [void]
|
|
55
|
-
def on_disconnect
|
|
56
|
-
# Optional hook - default implementation does nothing
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spikard
|
|
4
|
+
# Base class for WebSocket message handlers.
|
|
5
|
+
#
|
|
6
|
+
# Implement this class to handle WebSocket connections and messages.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# class ChatHandler < Spikard::WebSocketHandler
|
|
10
|
+
# def handle_message(message)
|
|
11
|
+
# # Echo message back
|
|
12
|
+
# message
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# def on_connect
|
|
16
|
+
# puts "Client connected"
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# def on_disconnect
|
|
20
|
+
# puts "Client disconnected"
|
|
21
|
+
# end
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# app = Spikard::App.new
|
|
25
|
+
#
|
|
26
|
+
# app.websocket('/chat') do
|
|
27
|
+
# ChatHandler.new
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
# app.run
|
|
31
|
+
class WebSocketHandler
|
|
32
|
+
# Handle an incoming WebSocket message.
|
|
33
|
+
#
|
|
34
|
+
# @param message [Hash] Parsed JSON message from the client
|
|
35
|
+
# @return [Hash, nil] Optional response message to send back to the client.
|
|
36
|
+
# Return nil to not send a response.
|
|
37
|
+
def handle_message(message)
|
|
38
|
+
raise NotImplementedError, "#{self.class.name} must implement #handle_message"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Called when a client connects.
|
|
42
|
+
#
|
|
43
|
+
# Override this method to perform initialization when a client connects.
|
|
44
|
+
#
|
|
45
|
+
# @return [void]
|
|
46
|
+
def on_connect
|
|
47
|
+
# Optional hook - default implementation does nothing
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Called when a client disconnects.
|
|
51
|
+
#
|
|
52
|
+
# Override this method to perform cleanup when a client disconnects.
|
|
53
|
+
#
|
|
54
|
+
# @return [void]
|
|
55
|
+
def on_disconnect
|
|
56
|
+
# Optional hook - default implementation does nothing
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
data/lib/spikard.rb
CHANGED
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Main Ruby namespace for the Spikard bindings.
|
|
4
|
-
module Spikard
|
|
5
|
-
end
|
|
6
|
-
|
|
7
|
-
begin
|
|
8
|
-
require 'json'
|
|
9
|
-
rescue LoadError
|
|
10
|
-
# Fallback to pure-Ruby implementation when native JSON extension is unavailable
|
|
11
|
-
require 'json/pure'
|
|
12
|
-
end
|
|
13
|
-
require_relative 'spikard/version'
|
|
14
|
-
require_relative 'spikard/config'
|
|
15
|
-
require_relative 'spikard/response'
|
|
16
|
-
require_relative 'spikard/streaming_response'
|
|
17
|
-
require_relative 'spikard/background'
|
|
18
|
-
require_relative 'spikard/schema'
|
|
19
|
-
require_relative 'spikard/websocket'
|
|
20
|
-
require_relative 'spikard/sse'
|
|
21
|
-
require_relative 'spikard/upload_file'
|
|
22
|
-
require_relative 'spikard/converters'
|
|
23
|
-
require_relative 'spikard/provide'
|
|
24
|
-
require_relative 'spikard/handler_wrapper'
|
|
25
|
-
require_relative 'spikard/app'
|
|
26
|
-
require_relative 'spikard/testing'
|
|
27
|
-
|
|
28
|
-
begin
|
|
29
|
-
require 'spikard_rb'
|
|
30
|
-
rescue LoadError => e
|
|
31
|
-
raise LoadError, <<~MSG, e.backtrace
|
|
32
|
-
Failed to load the Spikard native extension (spikard_rb). Run `bundle exec rake ext:build` to compile it before executing tests.
|
|
33
|
-
Original error: #{e.message}
|
|
34
|
-
MSG
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# Convenience aliases and methods at top level
|
|
38
|
-
module Spikard
|
|
39
|
-
TestClient = Testing::TestClient
|
|
40
|
-
|
|
41
|
-
# Handler wrapper utilities
|
|
42
|
-
extend HandlerWrapper
|
|
43
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Main Ruby namespace for the Spikard bindings.
|
|
4
|
+
module Spikard
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
begin
|
|
8
|
+
require 'json'
|
|
9
|
+
rescue LoadError
|
|
10
|
+
# Fallback to pure-Ruby implementation when native JSON extension is unavailable
|
|
11
|
+
require 'json/pure'
|
|
12
|
+
end
|
|
13
|
+
require_relative 'spikard/version'
|
|
14
|
+
require_relative 'spikard/config'
|
|
15
|
+
require_relative 'spikard/response'
|
|
16
|
+
require_relative 'spikard/streaming_response'
|
|
17
|
+
require_relative 'spikard/background'
|
|
18
|
+
require_relative 'spikard/schema'
|
|
19
|
+
require_relative 'spikard/websocket'
|
|
20
|
+
require_relative 'spikard/sse'
|
|
21
|
+
require_relative 'spikard/upload_file'
|
|
22
|
+
require_relative 'spikard/converters'
|
|
23
|
+
require_relative 'spikard/provide'
|
|
24
|
+
require_relative 'spikard/handler_wrapper'
|
|
25
|
+
require_relative 'spikard/app'
|
|
26
|
+
require_relative 'spikard/testing'
|
|
27
|
+
|
|
28
|
+
begin
|
|
29
|
+
require 'spikard_rb'
|
|
30
|
+
rescue LoadError => e
|
|
31
|
+
raise LoadError, <<~MSG, e.backtrace
|
|
32
|
+
Failed to load the Spikard native extension (spikard_rb). Run `bundle exec rake ext:build` to compile it before executing tests.
|
|
33
|
+
Original error: #{e.message}
|
|
34
|
+
MSG
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Convenience aliases and methods at top level
|
|
38
|
+
module Spikard
|
|
39
|
+
TestClient = Testing::TestClient
|
|
40
|
+
|
|
41
|
+
# Handler wrapper utilities
|
|
42
|
+
extend HandlerWrapper
|
|
43
|
+
end
|