Connectors¶
Connectors are a core extension point of OnDemand Loop.
They encapsulate repository-specific behavior, allowing the application to support multiple external repository platforms such as Dataverse and Zenodo without modifying the core codebase.
Dataverse is currently the most complete and serves as the reference implementation.
Each connector is responsible for implementing the full lifecycle of interactions with a remote repository—such as URL parsing, dataset browsing, file listing, and upload/download operations.
Connector Deployment Model
Connectors are currently built and deployed as part of the main application. They are required at build time and are not dynamically pluggable.
While the architecture follows naming and loading conventions that could support plugin-based loading in the future, all connector logic, templates, and assets must currently reside within the application codebase and be included in the build.
Core Responsibilities¶
Every connector must implement logic to support:
- Repository URL parsing (identify and extract dataset/file components)
- Dataset browsing (for downloads and uploads)
- File listing within datasets
- Downloading individual files
- Selecting/creating datasets for upload
- Uploading files to the repository
If a particular feature is not supported by the remote repository, the connector is still expected to handle that case gracefully and provide meaningful feedback to the user.
Required Components¶
Based on analysis of existing implementations (Dataverse and Zenodo), a complete connector requires these components:
Core Infrastructure Components¶
ConnectorType
registration inapp/models/connector_type.rb
- URL Parser class in
app/services/<connector>/<connector>_url.rb
- Repository URL Resolver in
app/services/repo/resolvers/<connector>_resolver.rb
- Display Repository Controller Resolver in
app/connectors/<connector>/display_repo_controller_resolver.rb
Processor Classes (in app/connectors/<connector>/
)¶
DownloadConnectorProcessor
- Handles file downloads from the repositoryUploadConnectorProcessor
- Handles individual file uploadsUploadBundleConnectorProcessor
- Handles upload bundle operationsRepositorySettingsProcessor
- Manages repository configuration
Metadata Classes (in app/connectors/<connector>/
)¶
DownloadConnectorMetadata
- Stores download-specific metadataUploadBundleConnectorMetadata
- Stores upload bundle metadata
Handler Classes (in app/connectors/<connector>/handlers/
)¶
Handlers implement repository browsing and dataset/file selection workflows:
Landing
- Repository landing page (lists repositories/servers)- Collection handlers - Browse collections/communities
- Dataset handlers - Browse and select datasets
- File handlers - List files within datasets
- Upload handlers - Dataset creation and upload workflows
Supporting Services (in app/services/<connector>/
)¶
ApiService
- Base API service with authentication headersProjectService
- Creates projects and configures download files with connector specific metadata- Domain-specific services (e.g.,
DatasetService
,CollectionService
)
Views and Templates (in app/views/connectors/<connector>/
)¶
- Landing pages, dataset browsers, file listings
- Upload/download forms and progress views
- Breadcrumb navigation and search interfaces
All connector logic is dynamically resolved through ConnectorClassDispatcher
, which uses naming conventions to instantiate the correct connector-specific classes based on the ConnectorType
.
Creating a Connector¶
Follow these steps to implement a new connector (using figshare
as an example):
1. Register the Connector Type¶
Add the new connector to the TYPES
array in app/models/connector_type.rb
:
2. Create URL Parser¶
Implement URL parsing logic in app/services/figshare/figshare_url.rb
:
module Figshare
class FigshareUrl
TYPES = %w[figshare article file unknown].freeze
attr_reader :type, :article_id, :file_id
delegate :domain, :scheme, :port, to: :base
def self.parse(url)
base = Repo::RepoUrl.parse(url)
return nil unless base
new(base)
end
def figshare_url
base.server_url
end
private
def parse_type_and_ids
# Parse URL segments to extract article/file IDs
# Set @type, @article_id, @file_id based on URL pattern
end
end
end
3. Implement URL Resolution¶
Create a repository URL resolver app/services/repo/resolvers/figshare_resolver.rb
:
module Repo
module Resolvers
class FigshareResolver < Repo::BaseResolver
def priority
8_000 # Lower number = higher priority
end
def resolve(context)
return unless context.object_url
return if context.type
repo_url = Figshare::FigshareUrl.parse(context.object_url)
return unless repo_url&.domain
if responds_to_api?(context.http_client, repo_url)
success(context, repo_url.figshare_url)
end
end
private
def success(context, repo_base_url)
context.type = ConnectorType::FIGSHARE
context.repo_db.set(repo_base_url, type: ConnectorType::FIGSHARE)
end
end
end
end
4. Implement Core Processor Classes¶
Create the processor classes in app/connectors/figshare/
:
Download Processor (download_connector_processor.rb
):
module Figshare
class DownloadConnectorProcessor
include LoggingCommon
def initialize(file)
@file = file
@connector_metadata = file.connector_metadata
end
def download
# Implement download logic using repository API
# Return OpenStruct.new(status: FileStatus::SUCCESS, message: "...")
end
end
end
Upload Processors (upload_connector_processor.rb
, upload_bundle_connector_processor.rb
):
module Figshare
class UploadConnectorProcessor
def initialize(file)
@file = file
@connector_metadata = file.upload_bundle.connector_metadata
end
def upload
# Implement file upload logic
end
end
class UploadBundleConnectorProcessor
def initialize(upload_bundle)
@upload_bundle = upload_bundle
end
def process
# Implement batch upload processing
end
end
end
5. Add Metadata Classes¶
Create metadata classes in app/connectors/figshare/
:
module Figshare
class DownloadConnectorMetadata
delegate_missing_to :metadata
def initialize(download_file)
@metadata = ActiveSupport::OrderedOptions.new
@metadata.merge!(download_file.metadata.to_h.deep_symbolize_keys)
end
def to_h
metadata.to_h.deep_stringify_keys
end
end
class UploadBundleConnectorMetadata
def initialize(upload_bundle)
@metadata = upload_bundle.metadata.to_h.deep_symbolize_keys
end
def configured?
# Return true if all required metadata is present
end
end
end
6. Implement Display Controller Resolver¶
Create app/connectors/figshare/display_repo_controller_resolver.rb
:
module Figshare
class DisplayRepoControllerResolver
def initialize(object = nil)
# Required for ConnectorClassDispatcher interface
end
def get_controller_url(object_url)
figshare_url = Figshare::FigshareUrl.parse(object_url)
if figshare_url.article?
redirect_url = link_to_explore(ConnectorType::FIGSHARE, figshare_url,
type: 'articles', id: figshare_url.article_id)
else
redirect_url = link_to_landing(ConnectorType::FIGSHARE)
end
ConnectorResult.new(redirect_url: redirect_url, success: true)
end
end
end
7. Add Repository Settings Processor¶
Create app/connectors/figshare/repository_settings_processor.rb
:
module Figshare
class RepositorySettingsProcessor
def initialize(object = nil)
end
def process(repository_data)
# Process repository configuration settings
# Return ConnectorResult with success/failure status
end
end
end
8. Create Handler Classes¶
Implement handlers in app/connectors/figshare/handlers/
:
landing.rb
- Repository landing pagearticles.rb
- Browse articles/datasetscollections.rb
- Browse collections- Various upload handlers for dataset creation
Each handler should follow this pattern:
module Figshare::Handlers
class Articles
def initialize(object_id = nil)
@article_id = object_id
end
def params_schema
[:repo_url, :page, :query]
end
def show(request_params)
# Implement browsing logic
# Return ConnectorResult with template and locals
end
end
end
9. Add Supporting Services¶
Create services in app/services/figshare/
:
api_service.rb
- Base API serviceproject_service.rb
- Project and file creation- Domain-specific services for articles, collections, etc.
10. Create Views and Templates¶
Add view templates in app/views/connectors/figshare/
:
- Landing pages
- Article/collection browsers
- File listing views
- Upload forms
Implementation Patterns and Key Interfaces¶
Dynamic Class Loading¶
The ConnectorClassDispatcher
uses Ruby's constantize
method to dynamically load connector classes:
def self.load(module_name, class_name, object)
connector_class = "#{module_name.to_s.camelize}::#{class_name}"
connector_class.constantize.new(object)
rescue NameError
raise ConnectorNotSupported, "Invalid connector type #{module_name}. Class not found: #{connector_class}"
end
This requires strict adherence to naming conventions:
- Module name must match connector type (e.g., Dataverse
, Zenodo
)
- Class names must match expected patterns (e.g., DownloadConnectorProcessor
)
Handler Interface Pattern¶
Handlers must implement a consistent interface:
initialize(object_id = nil)
- Constructor with optional object IDparams_schema
- Array of allowed request parametersshow(request_params)
- Main action method returningConnectorResult
Processor Interface Pattern¶
Processors follow these patterns:
Download Processors:
- Must implement download()
method
- Should support cancellation via command registry
- Return status object with status
and message
fields
Upload Processors:
- Must implement upload()
method
- Should handle file verification (MD5 checksums)
- Support progress tracking and cancellation
Metadata Class Pattern¶
Metadata classes use delegate_missing_to
for flexible attribute access:
class DownloadConnectorMetadata
delegate_missing_to :metadata
def initialize(download_file)
@metadata = ActiveSupport::OrderedOptions.new
@metadata.merge!(download_file.metadata.to_h.deep_symbolize_keys)
end
def to_h
metadata.to_h.deep_stringify_keys
end
end
ConnectorResult Response Pattern¶
All handlers and resolvers should return ConnectorResult
objects:
ConnectorResult.new(
template: '/connectors/figshare/articles/show',
locals: { articles: articles, pagination: pagination },
resource: resource_object,
success: true,
message: { alert: "Error message" },
redirect_url: "/path/to/redirect"
)
URL Parsing Interface¶
URL parser classes should implement:
self.parse(url)
class method- Type detection methods (e.g.,
article?
,dataset?
) - ID extraction properties (e.g.,
dataset_id
,file_id
) - Server URL reconstruction method
Authentication Patterns¶
API services define authentication headers:
module Figshare
class ApiService
AUTH_HEADER = 'Authorization' # or connector-specific header
class UnauthorizedException < StandardError; end
class ApiKeyRequiredException < StandardError; end
end
end
Asset Support¶
Connectors may include CSS and JavaScript assets, but it's optional. Assets are deployed alongside the application and should be placed under namespaced folders:
app/assets/stylesheets/<connector_type>/dataset.scss
app/javascripts/controllers/<connector_type>/
These will be bundled and served with the application like any other asset.
Best Practices¶
Architecture and Design¶
- Use Dataverse as reference implementation - It has the most complete feature set and serves as the canonical example
- Follow naming conventions precisely - Dynamic class loading requires exact naming patterns for modules and classes
- Implement all required interfaces - Even if a repository doesn't support a feature, handle it gracefully with meaningful error messages
- Use Ruby modules for namespacing - All connector classes should be namespaced (e.g.,
Dataverse::
,Zenodo::
)
Error Handling and User Experience¶
- Implement graceful degradation - Handle unsupported features with clear user feedback
- Use ConnectorResult consistently - Return structured results with success/failure status and user messages
- Support operation cancellation - Register with
Command::CommandRegistry
for download/upload cancellation - Provide meaningful error messages - Use I18n keys for user-facing error messages
API Integration¶
- Implement proper authentication - Uploads and private/draft datasets typically require authorization
- Handle rate limiting and retries - Repository APIs may have usage limits
- Validate checksums when available - Verify file integrity using MD5 or other hashes provided by the repository
- Support pagination - Many repository APIs paginate results for large datasets
URL Parsing and Resolution¶
- Parse URLs comprehensively - Handle different URL formats the repository supports (permalinks, versioned URLs, etc.)
- Support URL reconstruction - Be able to build canonical URLs from parsed components
- Set appropriate resolver priorities - Lower numbers = higher priority in the resolver chain
- Test edge cases - Handle malformed URLs, missing components, and ambiguous patterns
Testing and Development¶
- Use mocks for automated testing - Create unit, integration, and end-to-end tests with mocked repository responses
- Test manually with real instances - Use live repository systems or locally running instances for manual validation
- Implement comprehensive test coverage - Test all processor classes, handlers, and metadata classes
- Document API-specific limitations - Help users understand what operations are supported
- Use logging extensively - Include LoggingCommon and log important operations for debugging
Performance Considerations¶
- Implement efficient file transfers - Use streaming uploads/downloads for large files
- Cache API responses when appropriate - Reduce API calls for static data
- Handle large datasets gracefully - Support pagination and lazy loading for file listings
- Implement progress tracking - Provide user feedback for long-running operations