Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/badges/code_issues.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
158 changes: 68 additions & 90 deletions src/ndi/+ndi/+ontology/RRID.m
Original file line number Diff line number Diff line change
@@ -1,114 +1,92 @@
% Location: +ndi/+ontology/RRID.m
% Final Version: Supports lookup by RRID component only. Removed name search path.

classdef RRID < ndi.ontology
% RRID - NDI Ontology object for Research Resource Identifiers (RRID).
% Inherits from ndi.ontology and implements lookupTermOrID for RRIDs
% using the scicrunch.org resolver API's .json endpoint.

methods
function obj = RRID()
% RRID - Constructor for the RRID ontology object.
end % constructor
% RRID - Constructor
end

function [id, name, definition, synonyms] = lookupTermOrID(obj, term_or_id_or_name)
% LOOKUPTERMORID - Looks up an RRID via scicrunch.org resolver endpoint using its ID component.
%
% [ID, NAME, DEFINITION, SYNONYMS] = lookupTermOrID(OBJ, TERM_OR_ID_OR_NAME)
%
% Overrides the base class method. Expects TERM_OR_ID_OR_NAME to be the
% remainder of the RRID after the 'RRID:' prefix (e.g., 'SCR_006472', 'AB_123456').
% It must resemble a valid RRID component structure.
%
% NOTE: Lookup by name (e.g., 'NCBI') is NOT currently supported for RRIDs
% due to lack of a confirmed reliable public search API endpoint accessible
% via standard web requests from MATLAB. Please provide the specific RRID
% component (e.g., SCR_006472).
%
% Queries the SciCrunch resolver endpoint: https://scicrunch.org/resolver/{RRID}.json
%
% Outputs:
% ID - The full RRID used for the lookup (e.g., 'RRID:SCR_006472').
% NAME - The name found for the resource (typically from item.name).
% DEFINITION - The description found (typically from item.description).
% SYNONYMS - Cell array of synonyms, if found within the item.synonyms structure.
%
% See also: ndi.ontology.lookup (static dispatcher)

original_input_remainder = strtrim(term_or_id_or_name);
id = ''; name = ''; definition = ''; synonyms = {};

% --- Check if input looks like a valid RRID identifier component ---
% Basic regex for common RRID patterns like XXX_YYY or XXX:YYY
isLikelyID = ~isempty(regexp(original_input_remainder, '^[A-Za-z]+[_:]\S+$', 'once'));

if ~isLikelyID
error('ndi:ontology:RRID:NameLookupUnsupported', ...
'Input "%s" does not look like a valid RRID component (e.g., SCR_001234 or AB_123456). Lookup by name is not currently supported for RRID. Please provide the specific RRID component after the RRID: prefix.', original_input_remainder);
end

% --- Proceed with ID Lookup ---
full_rrid = ['RRID:' original_input_remainder];
lookup_type_msg = sprintf('RRID "%s"', full_rrid); % For error context if helper fails
% We ignore the 5th and 6th outputs here for standard ontology compatibility
[id, name, definition, synonyms, ~, ~] = obj.performRridResolverLookup(full_rrid);
end

function [scientificName, commonName] = lookupSpecies(obj, term_or_id_or_name)
% LOOKUPSPECIES - Returns the scientific and common names for an RRID.
%
% [SCIENTIFIC, COMMON] = lookupSpecies(OBJ, RRID_COMPONENT)
%
% Example: [s, c] = obj.lookupSpecies('RGD_13508588')
% Returns: s = 'Rattus norvegicus', c = 'Rat'

id_remainder = strtrim(term_or_id_or_name);
full_rrid = ['RRID:' id_remainder];

try
% Call the private static helper using the full RRID
[id, name, definition, synonyms] = ndi.ontology.RRID.performRridResolverLookup(full_rrid);
[~, ~, ~, ~, scientificName, commonName] = obj.performRridResolverLookup(full_rrid);
catch ME
% Wrap errors from the helper
baseME = MException('ndi:ontology:RRID:IDLookupFailed', ...
'Lookup failed for %s.', lookup_type_msg);
baseME = addCause(baseME, ME); throw(baseME);
rethrow(ME);
end

end % function lookupTermOrID

end % methods
end
end

methods (Static, Access = private)
% --- Helper function for the actual RRID Lookup via Resolver ---
function [id, name, definition, synonyms] = performRridResolverLookup(full_rrid)
%PERFORMRRIDRESOLVERLOOKUP Fetches details using /resolver/{RRID}.json
arguments, full_rrid (1,:) char {mustBeNonempty}, end
id = ''; name = ''; definition = ''; synonyms = {}; % Initialize

apiUrlBase = 'https://scicrunch.org/resolver/';
try encoded_rrid_path_segment = urlencode(full_rrid);
catch ME_encode, error('ndi:ontology:RRID:EncodingError', 'Failed URL encode for %s: %s', full_rrid, ME_encode.message);
end
apiUrl = [apiUrlBase encoded_rrid_path_segment '.json'];

ua = 'Mozilla/5.0 (MATLAB NDI Ontology Lookup)';
options = weboptions('Timeout', 30, 'HeaderFields', {'Accept', 'application/json'; 'User-Agent', ua});
function [id, name, definition, synonyms, scientificName, commonName] = performRridResolverLookup(full_rrid)
id = ''; name = ''; definition = ''; synonyms = {};
scientificName = ''; commonName = '';

apiUrl = ['https://scicrunch.org/resolver/' urlencode(full_rrid) '.json'];
options = weboptions('Timeout', 30, 'HeaderFields', {'Accept', 'application/json'});

try
response = webread(apiUrl, options);

% Parse the response based on observed structure
if isstruct(response) && isfield(response, 'hits') && isfield(response.hits, 'hits') && ~isempty(response.hits.hits) && isstruct(response.hits.hits(1)) && isfield(response.hits.hits(1), 'x_source') && isstruct(response.hits.hits(1).x_source) && isfield(response.hits.hits(1).x_source, 'item') && isstruct(response.hits.hits(1).x_source.item)
item_data = response.hits.hits(1).x_source.item;
if isstruct(response) && isfield(response, 'hits') && ~isempty(response.hits.hits)
source = response.hits.hits(1).x_source;
item_data = source.item;
id = full_rrid;

if isfield(item_data, 'name') && ~isempty(item_data.name) && (ischar(item_data.name) || isstring(item_data.name)), name = strtrim(char(item_data.name)); else, name = ''; end
if isfield(item_data, 'description') && ~isempty(item_data.description) && (ischar(item_data.description) || isstring(item_data.description)), definition = strtrim(char(item_data.description)); else, definition = ''; end
synonyms = {};
if isfield(item_data, 'synonyms') && ~isempty(item_data.synonyms), syn_data = item_data.synonyms; if iscell(syn_data) && all(cellfun(@(c) ischar(c) || isstring(c), syn_data)), synonyms = cellfun(@char, syn_data, 'UniformOutput', false); elseif isstruct(syn_data), if isfield(syn_data, 'literal') && ~isempty(syn_data.literal), literal_data = syn_data.literal; if iscell(literal_data) && all(cellfun(@(c) ischar(c) || isstring(c), literal_data)), synonyms = cellfun(@char, literal_data, 'UniformOutput', false); elseif ischar(literal_data) || isstring(literal_data), synonyms = {char(literal_data)}; end; elseif isfield(syn_data, 'name'), name_data = {syn_data.name}; synonyms = cellfun(@char, name_data, 'UniformOutput', false); end; elseif ischar(syn_data) || isstring(syn_data), synonyms = {char(syn_data)}; end; if ~isempty(synonyms), synonyms = synonyms(~cellfun('isempty', synonyms)); if isempty(synonyms), synonyms = {}; end; end; end % End synonym parsing
if isempty(name) && isempty(definition), warning('ndi:ontology:RRID:LookupDataMissing', 'Queried %s successfully but key metadata (item.name, item.description) seems missing.', full_rrid); elseif isempty(name), warning('ndi:ontology:RRID:MissingName', 'Could not extract item.name for %s.', full_rrid);

% 1. Metadata
if isfield(item_data, 'name'), name = char(item_data.name); end
if isfield(item_data, 'description'), definition = char(item_data.description); end

% 2. Organism Data Extraction (Scientific and Common)
if isfield(source, 'organisms') && isfield(source.organisms, 'primary') && ~isempty(source.organisms.primary)
primary_info = source.organisms.primary(1);

if isfield(primary_info, 'species') && isfield(primary_info.species, 'name')
scientificName = char(primary_info.species.name);
end

if isfield(primary_info, 'common') && isfield(primary_info.common, 'name')
commonName = char(primary_info.common.name);
end

elseif isfield(item_data, 'species') && ~isempty(item_data.species)
% Fallback for cases like antibodies where species is a simple field
if iscell(item_data.species)
scientificName = cellfun(@char, item_data.species, 'UniformOutput', false);
else
scientificName = char(item_data.species);
end
end

% 3. Synonyms
if isfield(item_data, 'synonyms') && ~isempty(item_data.synonyms)
syn_data = item_data.synonyms;
if iscell(syn_data)
synonyms = cellfun(@char, syn_data, 'UniformOutput', false);
elseif isstruct(syn_data) && isfield(syn_data, 'name')
synonyms = {char(syn_data.name)};
end
end

elseif isstruct(response) && isfield(response, 'success') && ~response.success, err_msg = 'API indicated failure (success=false).'; if isfield(response,'errormsg') && ~isempty(response.errormsg), err_msg = response.errormsg; elseif isfield(response,'message') && ~isempty(response.message), err_msg = response.message; end; error('ndi:ontology:RRID:APIReportedFailure', 'SciCrunch API reported failure for RRID "%s": %s', full_rrid, err_msg);
else, error('ndi:ontology:RRID:InvalidResponse', 'Received unexpected JSON structure from SciCrunch /resolver/*.json API for %s.', full_rrid);
end
catch ME
isHttpError = contains(ME.identifier, 'MATLAB:webservices:HTTP'); is404Error = isHttpError && (contains(ME.message, '404') || contains(ME.message, 'Not Found'));
if is404Error, error('ndi:ontology:RRID:IDNotFound', 'RRID "%s" lookup failed via SciCrunch endpoint %s (404 Error).', full_rrid, apiUrlBase);
elseif contains(ME.message, 'Timeout'), error('ndi:ontology:RRID:APITimeout', 'SciCrunch API request timed out for RRID "%s".', full_rrid);
elseif isHttpError, error('ndi:ontology:RRID:APIStatusError', 'SciCrunch API returned HTTP error for RRID "%s": %s', full_rrid, ME.message);
else, baseME = MException('ndi:ontology:RRID:APIError', 'SciCrunch /resolver/*.json API lookup failed for RRID "%s".', full_rrid); baseME = addCause(baseME, ME); throw(baseME);
end
rethrow(ME);
end
end % function performRridResolverLookup

end % methods (Static, Access = private)

end % classdef RRID
end
end
end