fast-mcp-annotations 1.5.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 +7 -0
- data/CHANGELOG.md +115 -0
- data/LICENSE +21 -0
- data/README.md +440 -0
- data/lib/fast_mcp.rb +189 -0
- data/lib/generators/fast_mcp/install/install_generator.rb +50 -0
- data/lib/generators/fast_mcp/install/templates/application_resource.rb +5 -0
- data/lib/generators/fast_mcp/install/templates/application_tool.rb +5 -0
- data/lib/generators/fast_mcp/install/templates/fast_mcp_initializer.rb +42 -0
- data/lib/generators/fast_mcp/install/templates/sample_resource.rb +12 -0
- data/lib/generators/fast_mcp/install/templates/sample_tool.rb +23 -0
- data/lib/mcp/logger.rb +32 -0
- data/lib/mcp/railtie.rb +45 -0
- data/lib/mcp/resource.rb +210 -0
- data/lib/mcp/server.rb +499 -0
- data/lib/mcp/server_filtering.rb +80 -0
- data/lib/mcp/tool.rb +867 -0
- data/lib/mcp/transports/authenticated_rack_transport.rb +71 -0
- data/lib/mcp/transports/base_transport.rb +40 -0
- data/lib/mcp/transports/rack_transport.rb +627 -0
- data/lib/mcp/transports/stdio_transport.rb +62 -0
- data/lib/mcp/version.rb +5 -0
- metadata +151 -0
data/lib/fast_mcp.rb
ADDED
@@ -0,0 +1,189 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Fast MCP - A Ruby Implementation of the Model Context Protocol (Server-side)
|
4
|
+
# https://modelcontextprotocol.io/introduction
|
5
|
+
|
6
|
+
# Define the MCP module
|
7
|
+
module FastMcp
|
8
|
+
class << self
|
9
|
+
attr_accessor :server
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# Require the core components
|
14
|
+
require_relative 'mcp/tool'
|
15
|
+
require_relative 'mcp/server'
|
16
|
+
require_relative 'mcp/resource'
|
17
|
+
require_relative 'mcp/railtie' if defined?(Rails::Railtie)
|
18
|
+
|
19
|
+
# Load generators if Rails is available
|
20
|
+
require_relative 'generators/fast_mcp/install/install_generator' if defined?(Rails::Generators)
|
21
|
+
|
22
|
+
# Require all transport files
|
23
|
+
require_relative 'mcp/transports/base_transport'
|
24
|
+
Dir[File.join(File.dirname(__FILE__), 'mcp/transports', '*.rb')].each do |file|
|
25
|
+
require file
|
26
|
+
end
|
27
|
+
|
28
|
+
# Version information
|
29
|
+
require_relative 'mcp/version'
|
30
|
+
|
31
|
+
# Convenience method to create a Rack middleware
|
32
|
+
module FastMcp
|
33
|
+
# Create a Rack middleware for the MCP server
|
34
|
+
# @param app [#call] The Rack application
|
35
|
+
# @param options [Hash] Options for the middleware
|
36
|
+
# @option options [String] :name The name of the server
|
37
|
+
# @option options [String] :version The version of the server
|
38
|
+
# @option options [String] :path_prefix The path prefix for the MCP endpoints
|
39
|
+
# @option options [String] :messages_route The route for the messages endpoint
|
40
|
+
# @option options [String] :sse_route The route for the SSE endpoint
|
41
|
+
# @option options [Logger] :logger The logger to use
|
42
|
+
# @option options [Array<String,Regexp>] :allowed_origins List of allowed origins for DNS rebinding protection
|
43
|
+
# @yield [server] A block to configure the server
|
44
|
+
# @yieldparam server [FastMcp::Server] The server to configure
|
45
|
+
# @return [#call] The Rack middleware
|
46
|
+
def self.rack_middleware(app, options = {})
|
47
|
+
name = options.delete(:name) || 'mcp-server'
|
48
|
+
version = options.delete(:version) || '1.0.0'
|
49
|
+
logger = options.delete(:logger) || Logger.new
|
50
|
+
|
51
|
+
server = FastMcp::Server.new(name: name, version: version, logger: logger)
|
52
|
+
yield server if block_given?
|
53
|
+
|
54
|
+
# Store the server in the Sinatra settings if available
|
55
|
+
app.settings.set(:mcp_server, server) if app.respond_to?(:settings) && app.settings.respond_to?(:mcp_server=)
|
56
|
+
|
57
|
+
# Store the server in the FastMcp module
|
58
|
+
self.server = server
|
59
|
+
|
60
|
+
server.start_rack(app, options)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Create a Rack middleware for the MCP server with authentication
|
64
|
+
# @param app [#call] The Rack application
|
65
|
+
# @param options [Hash] Options for the middleware
|
66
|
+
# @option options [String] :name The name of the server
|
67
|
+
# @option options [String] :version The version of the server
|
68
|
+
# @option options [String] :auth_token The authentication token
|
69
|
+
# @option options [Array<String,Regexp>] :allowed_origins List of allowed origins for DNS rebinding protection
|
70
|
+
# @yield [server] A block to configure the server
|
71
|
+
# @yieldparam server [FastMcp::Server] The server to configure
|
72
|
+
# @return [#call] The Rack middleware
|
73
|
+
def self.authenticated_rack_middleware(app, options = {})
|
74
|
+
name = options.delete(:name) || 'mcp-server'
|
75
|
+
version = options.delete(:version) || '1.0.0'
|
76
|
+
logger = options.delete(:logger) || Logger.new
|
77
|
+
|
78
|
+
server = FastMcp::Server.new(name: name, version: version, logger: logger)
|
79
|
+
yield server if block_given?
|
80
|
+
|
81
|
+
# Store the server in the FastMcp module
|
82
|
+
self.server = server
|
83
|
+
|
84
|
+
server.start_authenticated_rack(app, options)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Register a tool with the MCP server
|
88
|
+
# @param tool [FastMcp::Tool] The tool to register
|
89
|
+
# @return [FastMcp::Tool] The registered tool
|
90
|
+
def self.register_tool(tool)
|
91
|
+
self.server ||= FastMcp::Server.new(name: 'mcp-server', version: '1.0.0')
|
92
|
+
self.server.register_tool(tool)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Register multiple tools at once
|
96
|
+
# @param tools [Array<FastMcp::Tool>] The tools to register
|
97
|
+
# @return [Array<FastMcp::Tool>] The registered tools
|
98
|
+
def self.register_tools(*tools)
|
99
|
+
self.server ||= FastMcp::Server.new(name: 'mcp-server', version: '1.0.0')
|
100
|
+
self.server.register_tools(*tools)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Register a resource with the MCP server
|
104
|
+
# @param resource [FastMcp::Resource] The resource to register
|
105
|
+
# @return [FastMcp::Resource] The registered resource
|
106
|
+
def self.register_resource(resource)
|
107
|
+
self.server ||= FastMcp::Server.new(name: 'mcp-server', version: '1.0.0')
|
108
|
+
self.server.register_resource(resource)
|
109
|
+
end
|
110
|
+
|
111
|
+
# Register multiple resources at once
|
112
|
+
# @param resources [Array<FastMcp::Resource>] The resources to register
|
113
|
+
# @return [Array<FastMcp::Resource>] The registered resources
|
114
|
+
def self.register_resources(*resources)
|
115
|
+
self.server ||= FastMcp::Server.new(name: 'mcp-server', version: '1.0.0')
|
116
|
+
self.server.register_resources(*resources)
|
117
|
+
end
|
118
|
+
|
119
|
+
# Mount the MCP middleware in a Rails application
|
120
|
+
# @param app [Rails::Application] The Rails application
|
121
|
+
# @param options [Hash] Options for the middleware
|
122
|
+
# @option options [String] :name The name of the server
|
123
|
+
# @option options [String] :version The version of the server
|
124
|
+
# @option options [String] :path_prefix The path prefix for the MCP endpoints
|
125
|
+
# @option options [String] :messages_route The route for the messages endpoint
|
126
|
+
# @option options [String] :sse_route The route for the SSE endpoint
|
127
|
+
# @option options [Logger] :logger The logger to use
|
128
|
+
# @option options [Boolean] :authenticate Whether to use authentication
|
129
|
+
# @option options [String] :auth_token The authentication token
|
130
|
+
# @option options [Array<String,Regexp>] :allowed_origins List of allowed origins for DNS rebinding protection
|
131
|
+
# @yield [server] A block to configure the server
|
132
|
+
# @yieldparam server [FastMcp::Server] The server to configure
|
133
|
+
# @return [#call] The Rack middleware
|
134
|
+
def self.mount_in_rails(app, options = {})
|
135
|
+
# Default options
|
136
|
+
name = options.delete(:name) || app.class.module_parent_name.underscore.dasherize
|
137
|
+
version = options.delete(:version) || '1.0.0'
|
138
|
+
logger = options[:logger] || Rails.logger
|
139
|
+
path_prefix = options.delete(:path_prefix) || '/mcp'
|
140
|
+
messages_route = options.delete(:messages_route) || 'messages'
|
141
|
+
sse_route = options.delete(:sse_route) || 'sse'
|
142
|
+
authenticate = options.delete(:authenticate) || false
|
143
|
+
allowed_origins = options[:allowed_origins] || default_rails_allowed_origins(app)
|
144
|
+
allowed_ips = options[:allowed_ips] || FastMcp::Transports::RackTransport::DEFAULT_ALLOWED_IPS
|
145
|
+
|
146
|
+
options[:localhost_only] = Rails.env.local? if options[:localhost_only].nil?
|
147
|
+
options[:allowed_ips] = allowed_ips
|
148
|
+
options[:logger] = logger
|
149
|
+
options[:allowed_origins] = allowed_origins
|
150
|
+
|
151
|
+
# Create or get the server
|
152
|
+
self.server = FastMcp::Server.new(name: name, version: version, logger: logger)
|
153
|
+
yield self.server if block_given?
|
154
|
+
|
155
|
+
# Choose the right middleware based on authentication
|
156
|
+
self.server.transport_klass = if authenticate
|
157
|
+
FastMcp::Transports::AuthenticatedRackTransport
|
158
|
+
else
|
159
|
+
FastMcp::Transports::RackTransport
|
160
|
+
end
|
161
|
+
|
162
|
+
# Insert the middleware in the Rails middleware stack
|
163
|
+
app.middleware.use(
|
164
|
+
self.server.transport_klass,
|
165
|
+
self.server,
|
166
|
+
options.merge(path_prefix: path_prefix, messages_route: messages_route, sse_route: sse_route)
|
167
|
+
)
|
168
|
+
end
|
169
|
+
|
170
|
+
def self.default_rails_allowed_origins(rail_app)
|
171
|
+
hosts = rail_app.config.hosts
|
172
|
+
|
173
|
+
hosts.map do |host|
|
174
|
+
if host.is_a?(String) && host.start_with?('.')
|
175
|
+
# Convert .domain to domain and *.domain
|
176
|
+
host_without_dot = host[1..]
|
177
|
+
[host_without_dot, Regexp.new(".*\.#{host_without_dot}")] # rubocop:disable Style/RedundantStringEscape
|
178
|
+
else
|
179
|
+
host
|
180
|
+
end
|
181
|
+
end.flatten.compact
|
182
|
+
end
|
183
|
+
|
184
|
+
# Notify the server that a resource has been updated
|
185
|
+
# @param uri [String] The URI of the resource
|
186
|
+
def self.notify_resource_updated(uri)
|
187
|
+
self.server.notify_resource_updated(uri)
|
188
|
+
end
|
189
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/generators/base'
|
4
|
+
|
5
|
+
module FastMcp
|
6
|
+
module Generators
|
7
|
+
class InstallGenerator < Rails::Generators::Base
|
8
|
+
source_root File.expand_path('templates', __dir__)
|
9
|
+
|
10
|
+
desc 'Creates a FastMcp initializer for Rails applications'
|
11
|
+
|
12
|
+
def copy_initializer
|
13
|
+
template 'fast_mcp_initializer.rb', 'config/initializers/fast_mcp.rb'
|
14
|
+
end
|
15
|
+
|
16
|
+
def create_directories
|
17
|
+
empty_directory 'app/tools'
|
18
|
+
empty_directory 'app/resources'
|
19
|
+
end
|
20
|
+
|
21
|
+
def copy_application_tool
|
22
|
+
template 'application_tool.rb', 'app/tools/application_tool.rb'
|
23
|
+
end
|
24
|
+
|
25
|
+
def copy_application_resource
|
26
|
+
template 'application_resource.rb', 'app/resources/application_resource.rb'
|
27
|
+
end
|
28
|
+
|
29
|
+
def copy_sample_tool
|
30
|
+
template 'sample_tool.rb', 'app/tools/sample_tool.rb'
|
31
|
+
end
|
32
|
+
|
33
|
+
def copy_sample_resource
|
34
|
+
template 'sample_resource.rb', 'app/resources/sample_resource.rb'
|
35
|
+
end
|
36
|
+
|
37
|
+
def display_post_install_message
|
38
|
+
say "\n========================================================="
|
39
|
+
say 'FastMcp was successfully installed! 🎉'
|
40
|
+
say "=========================================================\n"
|
41
|
+
say 'You can now create:'
|
42
|
+
say ' • Tools in app/tools/'
|
43
|
+
say ' • Resources in app/resources/'
|
44
|
+
say "\n"
|
45
|
+
say 'Check config/initializers/fast_mcp.rb to configure the middleware.'
|
46
|
+
say "=========================================================\n"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# FastMcp - Model Context Protocol for Rails
|
4
|
+
# This initializer sets up the MCP middleware in your Rails application.
|
5
|
+
#
|
6
|
+
# In Rails applications, you can use:
|
7
|
+
# - ActionTool::Base as an alias for FastMcp::Tool
|
8
|
+
# - ActionResource::Base as an alias for FastMcp::Resource
|
9
|
+
#
|
10
|
+
# All your tools should inherit from ApplicationTool which already uses ActionTool::Base,
|
11
|
+
# and all your resources should inherit from ApplicationResource which uses ActionResource::Base.
|
12
|
+
|
13
|
+
# Mount the MCP middleware in your Rails application
|
14
|
+
# You can customize the options below to fit your needs.
|
15
|
+
require 'fast_mcp'
|
16
|
+
|
17
|
+
FastMcp.mount_in_rails(
|
18
|
+
Rails.application,
|
19
|
+
name: Rails.application.class.module_parent_name.underscore.dasherize,
|
20
|
+
version: '1.0.0',
|
21
|
+
path_prefix: '/mcp', # This is the default path prefix
|
22
|
+
messages_route: 'messages', # This is the default route for the messages endpoint
|
23
|
+
sse_route: 'sse' # This is the default route for the SSE endpoint
|
24
|
+
# Add allowed origins below, it defaults to Rails.application.config.hosts
|
25
|
+
# allowed_origins: ['localhost', '127.0.0.1', '[::1]', 'example.com', /.*\.example\.com/],
|
26
|
+
# localhost_only: true, # Set to false to allow connections from other hosts
|
27
|
+
# whitelist specific ips to if you want to run on localhost and allow connections from other IPs
|
28
|
+
# allowed_ips: ['127.0.0.1', '::1']
|
29
|
+
# authenticate: true, # Uncomment to enable authentication
|
30
|
+
# auth_token: 'your-token', # Required if authenticate: true
|
31
|
+
) do |server|
|
32
|
+
Rails.application.config.after_initialize do
|
33
|
+
# FastMcp will automatically discover and register:
|
34
|
+
# - All classes that inherit from ApplicationTool (which uses ActionTool::Base)
|
35
|
+
# - All classes that inherit from ApplicationResource (which uses ActionResource::Base)
|
36
|
+
server.register_tools(*ApplicationTool.descendants)
|
37
|
+
server.register_resources(*ApplicationResource.descendants)
|
38
|
+
# alternatively, you can register tools and resources manually:
|
39
|
+
# server.register_tool(MyTool)
|
40
|
+
# server.register_resource(MyResource)
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class SampleResource < ApplicationResource
|
4
|
+
uri 'examples/users'
|
5
|
+
resource_name 'Users'
|
6
|
+
description 'A user resource for demonstration'
|
7
|
+
mime_type 'application/json'
|
8
|
+
|
9
|
+
def content
|
10
|
+
JSON.generate(User.all.as_json)
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class SampleTool < ApplicationTool
|
4
|
+
description 'Greet a user'
|
5
|
+
|
6
|
+
# Optional: Add annotations to provide hints about the tool's behavior
|
7
|
+
# annotations(
|
8
|
+
# title: 'User Greeting',
|
9
|
+
# read_only_hint: true, # This tool only reads data
|
10
|
+
# open_world_hint: false # This tool only accesses the local database
|
11
|
+
# )
|
12
|
+
|
13
|
+
arguments do
|
14
|
+
required(:id).filled(:integer).description('ID of the user to greet')
|
15
|
+
optional(:prefix).filled(:string).description('Prefix to add to the greeting')
|
16
|
+
end
|
17
|
+
|
18
|
+
def call(id:, prefix: 'Hey')
|
19
|
+
user = User.find(id)
|
20
|
+
|
21
|
+
"#{prefix} #{user.first_name} !"
|
22
|
+
end
|
23
|
+
end
|
data/lib/mcp/logger.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This class is not used yet.
|
4
|
+
module FastMcp
|
5
|
+
class Logger < Logger
|
6
|
+
def initialize(transport: :stdio)
|
7
|
+
@client_initialized = false
|
8
|
+
@transport = transport
|
9
|
+
|
10
|
+
# we don't want to log to stdout if we're using the stdio transport
|
11
|
+
super($stdout) unless stdio_transport?
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_accessor :transport, :client_initialized
|
15
|
+
alias client_initialized? client_initialized
|
16
|
+
|
17
|
+
def stdio_transport?
|
18
|
+
transport == :stdio
|
19
|
+
end
|
20
|
+
|
21
|
+
def add(severity, message = nil, progname = nil, &block)
|
22
|
+
return if stdio_transport? # we don't want to log to stdout if we're using the stdio transport
|
23
|
+
|
24
|
+
# TODO: implement logging as the specification requires
|
25
|
+
super
|
26
|
+
end
|
27
|
+
|
28
|
+
def rack_transport?
|
29
|
+
transport == :rack
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/mcp/railtie.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
require 'fileutils'
|
5
|
+
require_relative '../mcp/server'
|
6
|
+
|
7
|
+
# Create ActionTool and ActionResource modules at load time
|
8
|
+
unless defined?(ActionTool)
|
9
|
+
module ::ActionTool
|
10
|
+
Base = FastMcp::Tool
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
unless defined?(ActionResource)
|
15
|
+
module ::ActionResource
|
16
|
+
Base = FastMcp::Resource
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module FastMcp
|
21
|
+
# Railtie for integrating Fast MCP with Rails applications
|
22
|
+
class Railtie < Rails::Railtie
|
23
|
+
# Add tools and resources directories to autoload paths
|
24
|
+
initializer 'fast_mcp.setup_autoload_paths' do |app|
|
25
|
+
app.config.autoload_paths += %W[
|
26
|
+
#{app.root}/app/tools
|
27
|
+
#{app.root}/app/resources
|
28
|
+
]
|
29
|
+
end
|
30
|
+
|
31
|
+
# Auto-register all tools and resources after the application is fully loaded
|
32
|
+
config.after_initialize do
|
33
|
+
# Load all files in app/tools and app/resources directories
|
34
|
+
Dir[Rails.root.join('app', 'tools', '**', '*.rb')].each { |f| require f }
|
35
|
+
Dir[Rails.root.join('app', 'resources', '**', '*.rb')].each { |f| require f }
|
36
|
+
end
|
37
|
+
|
38
|
+
# Add rake tasks
|
39
|
+
rake_tasks do
|
40
|
+
# Path to the tasks directory in the gem
|
41
|
+
path = File.expand_path('../tasks', __dir__)
|
42
|
+
Dir.glob("#{path}/**/*.rake").each { |f| load f }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/mcp/resource.rb
ADDED
@@ -0,0 +1,210 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'base64'
|
5
|
+
require 'mime/types'
|
6
|
+
require 'addressable/template'
|
7
|
+
|
8
|
+
module FastMcp
|
9
|
+
# Resource class for MCP Resources feature
|
10
|
+
# Represents a resource that can be exposed to clients
|
11
|
+
class Resource
|
12
|
+
class << self
|
13
|
+
attr_accessor :server
|
14
|
+
|
15
|
+
# Define URI for this resource
|
16
|
+
# @param value [String, nil] The URI for this resource
|
17
|
+
# @return [String] The URI for this resource
|
18
|
+
def uri(value = nil)
|
19
|
+
@uri = value if value
|
20
|
+
|
21
|
+
@uri || (superclass.respond_to?(:uri) ? superclass.uri : nil)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Variabilize the URI with the given params
|
25
|
+
# @param params [Hash] The parameters to variabilize the URI with
|
26
|
+
# @return [String] The variabilized URI
|
27
|
+
def variabilized_uri(params = {})
|
28
|
+
addressable_template.partial_expand(params).pattern
|
29
|
+
end
|
30
|
+
|
31
|
+
# Get the Addressable::Template for this resource
|
32
|
+
# @return [Addressable::Template] The Addressable::Template for this resource
|
33
|
+
def addressable_template
|
34
|
+
@addressable_template ||= Addressable::Template.new(uri)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Get the template variables for this resource
|
38
|
+
# @return [Array] The template variables for this resource
|
39
|
+
def template_variables
|
40
|
+
addressable_template.variables
|
41
|
+
end
|
42
|
+
|
43
|
+
# Check if this resource has a templated URI
|
44
|
+
# @return [Boolean] true if the URI contains template parameters
|
45
|
+
def templated?
|
46
|
+
template_variables.any?
|
47
|
+
end
|
48
|
+
|
49
|
+
# Check if this resource has a non-templated URI
|
50
|
+
# @return [Boolean] true if the URI does not contain template parameters
|
51
|
+
def non_templated?
|
52
|
+
!templated?
|
53
|
+
end
|
54
|
+
|
55
|
+
# Match the given URI against the resource's addressable template
|
56
|
+
# @param uri [String] The URI to match
|
57
|
+
# @return [Addressable::Template::MatchData, nil] The match data if the URI matches, nil otherwise
|
58
|
+
def match(uri)
|
59
|
+
addressable_template.match(uri)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Initialize a new instance from the given URI
|
63
|
+
# @param uri [String] The URI to initialize from
|
64
|
+
# @return [Resource] A new resource instance
|
65
|
+
def initialize_from_uri(uri)
|
66
|
+
new(params_from_uri(uri))
|
67
|
+
end
|
68
|
+
|
69
|
+
# Get the parameters from the given URI
|
70
|
+
# @param uri [String] The URI to get the parameters from
|
71
|
+
# @return [Hash] The parameters from the URI
|
72
|
+
def params_from_uri(uri)
|
73
|
+
match(uri).mapping.transform_keys(&:to_sym)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Define name for this resource
|
77
|
+
# @param value [String, nil] The name for this resource
|
78
|
+
# @return [String] The name for this resource
|
79
|
+
def resource_name(value = nil)
|
80
|
+
@name = value if value
|
81
|
+
@name || (superclass.respond_to?(:resource_name) ? superclass.resource_name : nil)
|
82
|
+
end
|
83
|
+
|
84
|
+
alias original_name name
|
85
|
+
def name
|
86
|
+
return resource_name if resource_name
|
87
|
+
|
88
|
+
original_name
|
89
|
+
end
|
90
|
+
|
91
|
+
# Define description for this resource
|
92
|
+
# @param value [String, nil] The description for this resource
|
93
|
+
# @return [String] The description for this resource
|
94
|
+
def description(value = nil)
|
95
|
+
@description = value if value
|
96
|
+
@description || (superclass.respond_to?(:description) ? superclass.description : nil)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Define MIME type for this resource
|
100
|
+
# @param value [String, nil] The MIME type for this resource
|
101
|
+
# @return [String] The MIME type for this resource
|
102
|
+
def mime_type(value = nil)
|
103
|
+
@mime_type = value if value
|
104
|
+
@mime_type || (superclass.respond_to?(:mime_type) ? superclass.mime_type : nil)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Get the resource metadata (without content)
|
108
|
+
# @return [Hash] Resource metadata
|
109
|
+
def metadata
|
110
|
+
if templated?
|
111
|
+
{
|
112
|
+
uriTemplate: uri,
|
113
|
+
name: resource_name,
|
114
|
+
description: description,
|
115
|
+
mimeType: mime_type
|
116
|
+
}.compact
|
117
|
+
else
|
118
|
+
{
|
119
|
+
uri: uri,
|
120
|
+
name: resource_name,
|
121
|
+
description: description,
|
122
|
+
mimeType: mime_type
|
123
|
+
}.compact
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Load content from a file (class method)
|
128
|
+
# @param file_path [String] Path to the file
|
129
|
+
# @return [Resource] New resource instance with content loaded from file
|
130
|
+
def from_file(file_path, name: nil, description: nil)
|
131
|
+
file_uri = "file://#{File.absolute_path(file_path)}"
|
132
|
+
file_name = name || File.basename(file_path)
|
133
|
+
|
134
|
+
# Create a resource subclass on the fly
|
135
|
+
Class.new(self) do
|
136
|
+
uri file_uri
|
137
|
+
resource_name file_name
|
138
|
+
description description if description
|
139
|
+
|
140
|
+
# Auto-detect mime type
|
141
|
+
extension = File.extname(file_path)
|
142
|
+
unless extension.empty?
|
143
|
+
detected_types = MIME::Types.type_for(extension)
|
144
|
+
mime_type detected_types.first.to_s unless detected_types.empty?
|
145
|
+
end
|
146
|
+
|
147
|
+
# Override content method to load from file
|
148
|
+
define_method :content do
|
149
|
+
if binary?
|
150
|
+
File.binread(file_path)
|
151
|
+
else
|
152
|
+
File.read(file_path)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Initialize with instance variables
|
160
|
+
# @param params [Hash] The parameters for this resource instance
|
161
|
+
def initialize(params = {})
|
162
|
+
@params = params
|
163
|
+
end
|
164
|
+
|
165
|
+
# URI of the resource - delegates to class method
|
166
|
+
# @return [String, nil] The URI for this resource
|
167
|
+
def uri
|
168
|
+
self.class.uri
|
169
|
+
end
|
170
|
+
|
171
|
+
# Name of the resource - delegates to class method
|
172
|
+
# @return [String, nil] The name for this resource
|
173
|
+
def name
|
174
|
+
self.class.resource_name
|
175
|
+
end
|
176
|
+
|
177
|
+
# Description of the resource - delegates to class method
|
178
|
+
# @return [String, nil] The description for this resource
|
179
|
+
def description
|
180
|
+
self.class.description
|
181
|
+
end
|
182
|
+
|
183
|
+
# MIME type of the resource - delegates to class method
|
184
|
+
# @return [String, nil] The MIME type for this resource
|
185
|
+
def mime_type
|
186
|
+
self.class.mime_type
|
187
|
+
end
|
188
|
+
|
189
|
+
# Get parameters from the URI template
|
190
|
+
# @return [Hash] The parameters extracted from the URI
|
191
|
+
attr_reader :params
|
192
|
+
|
193
|
+
# Method to be overridden by subclasses to dynamically generate content
|
194
|
+
# @return [String, nil] Generated content for this resource
|
195
|
+
def content
|
196
|
+
raise NotImplementedError, 'Subclasses must implement content'
|
197
|
+
end
|
198
|
+
|
199
|
+
# Check if the resource is binary
|
200
|
+
# @return [Boolean] true if the resource is binary, false otherwise
|
201
|
+
def binary?
|
202
|
+
return false if mime_type.nil?
|
203
|
+
|
204
|
+
!(mime_type.start_with?('text/') ||
|
205
|
+
mime_type == 'application/json' ||
|
206
|
+
mime_type == 'application/xml' ||
|
207
|
+
mime_type == 'application/javascript')
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|