diff --git a/Gemfile b/Gemfile index 3e50236..802b90f 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,5 @@ source 'https://rubygems.org' -gem 'activesupport', '~> 4.2.0' gem 'bel' gem 'bson_ext', '1.12.0' gem 'builder' @@ -23,6 +22,12 @@ gem 'sinatra-advanced-routes', :require => 'sinatra/advanced_routes' gem 'sinatra-contrib' gem 'sqlite3' +group :test do + gem 'faraday_middleware' + gem 'hyperclient' + gem 'rspec' +end + group :development do gem 'POpen4' gem 'pry', '~> 0.9.12' diff --git a/Gemfile.lock b/Gemfile.lock index 1a1c2e4..7e474be 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -27,8 +27,10 @@ GEM ast (2.0.0) backports (3.6.4) bel (0.3.0.beta5) + ffi (= 1.9.8) binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) + blankslate (3.1.3) bson (1.12.0) bson_ext (1.12.0) bson (~> 1.12.0) @@ -37,9 +39,47 @@ GEM timers (~> 4.0.0) coderay (1.1.0) debug_inspector (0.0.2) + diff-lcs (1.2.5) dot_hash (0.5.9) + faraday (0.9.1) + multipart-post (>= 1.2, < 3) + faraday-digestauth (0.2.1) + faraday (~> 0.7) + net-http-digest_auth (~> 1.4) + faraday_hal_middleware (0.0.1) + faraday_middleware (>= 0.9, < 0.10) + faraday_middleware (0.9.1) + faraday (>= 0.7.4, < 0.10) ffi (1.9.8) + foreman (0.78.0) + thor (~> 0.19.1) + formatador (0.2.5) + futuroscope (0.1.11) + guard (2.12.5) + formatador (>= 0.2.4) + listen (~> 2.7) + lumberjack (~> 1.0) + nenv (~> 0.1) + notiffany (~> 0.0) + pry (>= 0.9.12) + shellany (~> 0.0) + thor (>= 0.18.1) + guard-compat (1.2.1) + guard-ctags-bundler (1.4.0) + guard (>= 2.0) + guard-compat (>= 0.1.0) + guard-rake (1.0.0) + guard + rake hitimes (1.2.2) + hyperclient (0.7.0) + faraday (~> 0.8) + faraday-digestauth (~> 0.2) + faraday_hal_middleware (~> 0.0.1) + faraday_middleware (~> 0.9) + futuroscope (>= 0.0.10) + net-http-digest_auth (~> 1.2) + uri_template (~> 0.5) i18n (0.7.0) json (1.8.2) json_schema (0.5.0) @@ -47,20 +87,29 @@ GEM celluloid (>= 0.15.2) rb-fsevent (>= 0.9.3) rb-inotify (>= 0.9) + lumberjack (1.0.9) method_source (0.8.2) mini_portile (0.6.2) minitest (5.5.1) mongo (1.12.0) bson (= 1.12.0) multi_json (1.11.0) + multipart-post (2.0.0) + nenv (0.2.0) + net-http-digest_auth (1.4) nokogiri (1.6.6.2) mini_portile (~> 0.6.0) + notiffany (0.0.6) + nenv (~> 0.1) + shellany (~> 0.0) oat (0.4.6) activesupport oj (2.12.1) open4 (1.3.4) parser (2.2.0.3) ast (>= 1.1, < 3.0) + parslet (1.7.0) + blankslate (>= 2.0, <= 4.0) pry (0.9.12.6) coderay (~> 1.0) method_source (~> 0.8) @@ -80,6 +129,7 @@ GEM rack rack-test (0.6.3) rack (>= 1.0) + rake (10.4.2) rb-fsevent (0.9.4) rb-inotify (0.9.5) ffi (>= 0.5.0) @@ -87,7 +137,21 @@ GEM listen (~> 2.7, >= 2.7.3) ripper-tags (0.1.3) yajl-ruby - ruby-progressbar (1.7.4) + rspec (3.2.0) + rspec-core (~> 3.2.0) + rspec-expectations (~> 3.2.0) + rspec-mocks (~> 3.2.0) + rspec-core (3.2.3) + rspec-support (~> 3.2.0) + rspec-expectations (3.2.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.2.0) + rspec-mocks (3.2.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.2.0) + rspec-support (3.2.2) + ruby-progressbar (1.7.5) + shellany (0.0.1) sinatra (1.4.5) rack (~> 1.4) rack-protection (~> 1.4) @@ -110,6 +174,7 @@ GEM ruby-progressbar (~> 1.5) term-ansicolor (1.3.0) tins (~> 1.0) + thor (0.19.1) thread_safe (0.3.5) tilt (1.4.1) timers (4.0.1) @@ -117,6 +182,10 @@ GEM tins (1.3.5) tzinfo (1.2.2) thread_safe (~> 0.1) + uri_template (0.7.0) + vim-flavor (2.2.1) + parslet (~> 1.7) + thor (~> 0.19) xml_schema (0.2.0) yajl-ruby (1.2.1) @@ -125,11 +194,16 @@ PLATFORMS DEPENDENCIES POpen4 - activesupport (~> 4.2.0) bel bson_ext (= 1.12.0) builder dot_hash + faraday_middleware + foreman + guard + guard-ctags-bundler + guard-rake + hyperclient jrjackson (~> 0.2) json_schema kyotocabinet-ffi! @@ -148,9 +222,11 @@ DEPENDENCIES redlander! rerun ripper-tags + rspec sinatra sinatra-advanced-routes sinatra-contrib sqlite3 starscope term-ansicolor + vim-flavor diff --git a/app/resources/annotation.rb b/app/resources/annotation.rb index 31d8c7b..6500b5b 100644 --- a/app/resources/annotation.rb +++ b/app/resources/annotation.rb @@ -11,6 +11,7 @@ class AnnotationSerializer < BaseSerializer schema do type :annotation properties do |p| + p.rdf_uri item.uri p.name item.prefLabel p.prefix item.prefix p.domain item.domain diff --git a/app/resources/evidence.rb b/app/resources/evidence.rb index df5f74c..c6af00e 100644 --- a/app/resources/evidence.rb +++ b/app/resources/evidence.rb @@ -14,7 +14,9 @@ class EvidenceSerializer < BaseSerializer p.bel_statement item['bel_statement'] p.citation item['citation'] p.summary_text item['summary_text'] - p.biological_context item['biological_context'] + p.experiment_context prepare_experiment_context( + item['experiment_context'] + ) p.metadata item['metadata'] end @@ -24,6 +26,16 @@ class EvidenceSerializer < BaseSerializer private + def prepare_experiment_context(experiment_context) + experiment_context.each do |annotation| + if annotation['uri'] + annotation.delete('name') + annotation.delete('value') + end + end + experiment_context + end + def link_self(id) { :type => :evidence, diff --git a/app/resources/evidence_transform.rb b/app/resources/evidence_transform.rb index d721fcb..f30ff61 100644 --- a/app/resources/evidence_transform.rb +++ b/app/resources/evidence_transform.rb @@ -1,70 +1,70 @@ -require 'active_support' -require 'active_support/inflector/transliterate' +require 'bel' module OpenBEL module Resource module Evidence class AnnotationTransform - include ActiveSupport::Inflector + SERVER_PATTERN = %r{/api/annotations/([^/]*)/values/([^/]*)/?} + RDFURI_PATTERN = %r{/bel/namespace/([^/]*)/([^/]*)/?} URI_PATTERNS = [ - %r{/api/annotations/([^/]*)/values/([^/]*)/?}, #Route URI - %r{/bel/namespace/([^/]*)/([^/]*)/?}, #RDF URI + %r{/api/annotations/([^/]*)/values/([^/]*)/?}, + %r{/bel/namespace/([^/]*)/([^/]*)/?} ] + ANNOTATION_VALUE_URI = "%s/api/annotations/%s/values/%s" def initialize(annotation_api) @annotation_api = annotation_api end - def transform_evidence(evidence) - if evidence == nil - nil - else - context = evidence['biological_context'] - if context != nil - context.map! { |annotation| - transform_annotation(annotation) + def transform_evidence!(evidence, base_url) + if evidence + experiment_context = evidence.experiment_context + if experiment_context != nil + experiment_context.values.map! { |annotation| + transform_annotation(annotation, base_url) } end - - evidence end end - def transform_annotation(annotation) - if annotation['uri'] - transform_uri(annotation['uri']) - elsif annotation['name'] && annotation['value'] - name = annotation['name'] - value = annotation['value'] - transform_name_value(name, value) - else - nil + def transform_annotation(annotation, base_url) + if annotation[:uri] + transform_uri(annotation[:uri], base_url) + elsif annotation[:name] && annotation[:value] + name = annotation[:name] + value = annotation[:value] + transform_name_value(name, value, base_url) + elsif annotation.respond_to?(:each) + name = annotation[0] + value = annotation[1] + transform_name_value(name, value, base_url) end end private - def transform_uri(uri) + def transform_uri(uri, base_url) URI_PATTERNS.each do |pattern| match = pattern.match(uri) if match - return transform_name_value(match[1], match[2]) + return transform_name_value(match[1], match[2], base_url) end end end - def transform_name_value(name, value) - structured_annotation(name, value) || free_annotation(name, value) + def transform_name_value(name, value, base_url) + structured_annotation(name, value, base_url) || free_annotation(name, value) end - def structured_annotation(name, value) + def structured_annotation(name, value, base_url) annotation = @annotation_api.find_annotation(name) if annotation + annotation_label = annotation.prefLabel if value.respond_to?(:each) { - :name => annotation.prefLabel, + :name => annotation_label, :value => value.map { |v| mapped = @annotation_api.find_annotation_value(annotation, v) mapped ? mapped.prefLabel : v @@ -73,9 +73,11 @@ def structured_annotation(name, value) else annotation_value = @annotation_api.find_annotation_value(annotation, value) if annotation_value + value_label = annotation_value.prefLabel { :name => annotation.prefLabel, - :value => annotation_value.prefLabel + :value => annotation_value.prefLabel, + :uri => ANNOTATION_VALUE_URI % [base_url, annotation_label, value_label] } else { @@ -84,11 +86,6 @@ def structured_annotation(name, value) } end end - else - { - :name => name, - :value => value - } end end @@ -105,9 +102,9 @@ def normalize_annotation_name(name, options = {}) if name_s.empty? nil else - transliterate(name_s). + name_s. split(%r{[^a-zA-Z0-9]+}). - map { |word| word[0].upcase + word[1..-1] }. + map! { |word| word.capitalize }. join end end @@ -115,22 +112,27 @@ def normalize_annotation_name(name, options = {}) class AnnotationGroupingTransform - def transform_evidence(evidence) - context = evidence['biological_context'] - if context != nil - evidence['biological_context'] = context.group_by { |annotation| - annotation[:name] - }.values.map do |grouped_annotation| - { - :name => grouped_annotation.first[:name], - :value => grouped_annotation.flat_map { |annotation| - annotation[:value] + ExperimentContext = ::BEL::Model::ExperimentContext + + def transform_evidence!(evidence) + experiment_context = evidence.experiment_context + if experiment_context != nil + evidence.experiment_context = ExperimentContext.new( + experiment_context.group_by { |annotation| + annotation[:name] + }.values.map do |grouped_annotation| + { + :name => grouped_annotation.first[:name], + :value => grouped_annotation.flat_map { |annotation| + annotation[:value] + }, + :uri => grouped_annotation.flat_map { |annotation| + annotation[:uri] + } } - } - end + end + ) end - - evidence end end end diff --git a/app/routes/base.rb b/app/routes/base.rb index 192934f..b7bd94c 100644 --- a/app/routes/base.rb +++ b/app/routes/base.rb @@ -1,3 +1,4 @@ +require 'bel' require 'json_schema' require 'multi_json' require 'namespace/model' @@ -94,6 +95,12 @@ def validate_media_type!(content_type, options = {}) halt 415 unless valid end + def read_evidence + fmt = ::BEL::Extension::Format.formatters(request.media_type) + halt 415 unless fmt + ::BEL::Format.evidence(request.body, request.media_type) + end + def resolve_supported_content_type(request) accept_match = request.accept.find { |accept_entry| SPOKEN_CONTENT_TYPES.include?(accept_entry.to_s) diff --git a/app/routes/evidence.rb b/app/routes/evidence.rb index d85c2c1..ff85be2 100644 --- a/app/routes/evidence.rb +++ b/app/routes/evidence.rb @@ -30,28 +30,22 @@ def initialize(app) end post '/api/evidence' do - validate_media_type! "application/json", :profile => schema_url('evidence') - - evidence_obj = read_json - schema_validation = validate_schema(evidence_obj, :evidence) - unless schema_validation[0] - halt( - 400, - { 'Content-Type' => 'application/json' }, - render_json({ :status => 400, :msg => schema_validation[1].join("\n") }) - ) + _id = nil + read_evidence.each do |evidence| + @annotation_transform.transform_evidence!(evidence, base_url) + + # XXX Not sure we need to group values together. Instead we split + # multi-valued items into individual objects. + # Wait and see what breaks. + #@annotation_grouping_transform.transform_evidence!(evidence) + + facets = map_evidence_facets(evidence) + hash = evidence.to_h + hash[:bel_statement] = hash.fetch(:bel_statement, nil).to_s + hash[:facets] = facets + _id = @api.create_evidence(hash) end - # transformation - evidence = evidence_obj['evidence'] - evidence = @annotation_grouping_transform.transform_evidence( - @annotation_transform.transform_evidence(evidence) - ) - - evidence[:facets] = map_evidence_facets(evidence) - - _id = @api.create_evidence(evidence) - status 201 headers "Location" => "#{base_url}/api/evidence/#{_id}" end diff --git a/docs/openbel-api.raml b/docs/openbel-api.raml index c4cd123..63db0a6 100644 --- a/docs/openbel-api.raml +++ b/docs/openbel-api.raml @@ -252,7 +252,7 @@ schemas: "name": "Free Radic Biol Med 2000 Feb 1 28(3) 463-99", "id": "10699758" }, - "biological_context": { + "experiment_context": { "species": "9606", "disease": "leukemia" }, @@ -335,7 +335,7 @@ schemas: description: | A filter to apply to the evidence resource collection to narrow the resources retrieved. example: | - {"category":"biological_context","name":"Anatomy","value":"prostate gland"} + {"category":"experiment_context","name":"Anatomy","value":"prostate gland"} {"category":"fts","name":"search","value":"\"subdural hematoma\" carcinoma"} {"category":"fts","name":"search","value":"homatoma -carcinoma"} required: false @@ -358,7 +358,7 @@ schemas: - `filter` ```json { - "category":"biological_context", + "category":"experiment_context", "name":"Anatomy", "value":"prostate gland" } @@ -377,7 +377,7 @@ schemas: "name": "Landes Bioscience - Altered Integrin Expression in Three Common Types of Human Cancer=Breast Cancer", "id": "Landes Bioscience - Altered Integrin Expression in Three Common Types of Human Cancer=Breast Cancer" }, - "biological_context": { + "experiment_context": { "species": "9606" }, "metadata": { @@ -387,14 +387,14 @@ schemas: ] "facets": [ { - "category": "biological_context", + "category": "experiment_context", "name": "Species", "value": "9606", "filter": "{\"category\":\"context\",\"name\":\"Species\",\"value\":\"9606\"}", "count": 49071 }, { - "category": "biological_context", + "category": "experiment_context", "name": "Species", "value": "10090", "filter": "{\"category\":\"context\",\"name\":\"Species\",\"value\":\"10090\"}", @@ -429,14 +429,14 @@ schemas: "count": 18784 }, { - "category": "biological_context", + "category": "experiment_context", "name": "Species", "value": "10116", "filter": "{\"category\":\"context\",\"name\":\"Species\",\"value\":\"10116\"}", "count": 7091 }, { - "category": "biological_context", + "category": "experiment_context", "name": "Anatomy", "value": "liver", "filter": "{\"category\":\"context\",\"name\":\"Anatomy\",\"value\":\"liver\"}", @@ -477,7 +477,7 @@ schemas: "name": "Free Radic Biol Med 2000 Feb 1 28(3) 463-99", "id": "10699758" }, - "biological_context": { + "experiment_context": { "Species": "9606", "CellLine": "LNCAP cell", "Anatomy": "prostate gland", diff --git a/docs/schemas/evidence.schema.json b/docs/schemas/evidence.schema.json index f6c4acd..12eb168 100644 --- a/docs/schemas/evidence.schema.json +++ b/docs/schemas/evidence.schema.json @@ -1,131 +1,179 @@ { - "$schema": "http://json-schema.org/draft-04/schema", - "description": "DESCRIBE EVIDENCE", - "type": "object", + "$schema": "http://json-schema.org/draft-04/schema", + "description": "DESCRIBE EVIDENCE", + "type": "object", "additionalProperties": false, - "required": [ - "evidence" - ], + "required": ["evidence"], "properties": { "evidence": { - "type": "object", + "type": "object", "additionalProperties": false, - "required": [ - "bel_statement", - "citation" - ], + "required": ["bel_statement", "citation"], "properties": { "bel_statement": { - "type": "string", - "title": "BEL Statement", - "description": "A BEL Statement is an expression that represents knowledge of the existence of biological entities and relationships between them that are known to be observed within a particular context, based on some source of prior knowledge such as a scientific publication or newly generated experimental data." + "type": "string", + "title": "BEL Statement", + "description": "A BEL Statement is an expression that represents knowledge of the existence of biological entities and relationships between them that are known to be observed within a particular context, based on some source of prior knowledge such as a scientific publication or newly generated experimental data." }, "citation": { - "type": "object", - "title": "Citation", - "description": "The citation specifies the written source where the biological knowledge was referenced.", - "required": [ - "type", - "id" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "PubMed", - "Book", - "Journal", - "Online Resource", - "Other" - ], - "title": "Citation Type", - "description": "The citation type of the reference." - }, - "id": { - "type": ["string", "number"], - "title": "Citation ID", - "description": "The citation identifier (PubMed ID, ISBN, DOI, URL) of the reference." - }, - "name": { - "type": "string", - "title": "Citation Name", - "description": "The citation name of the reference." - }, - "date": { - "type": "string", - "title": "Citation Date", - "description": "The citation date of the reference." - }, - "authors": { - "type": "array", - "title": "Citation Authors", - "description": "The citation authors of the reference.", + "type": "object", + "title": "Citation", + "description": "The citation specifies the written source where the biological knowledge was referenced.", + "required": ["type", "id"], + "properties": { + "type": { + "type": "string", + "enum": ["PubMed", "Book", "Journal", "Online Resource", "Other"], + "title": "Citation Type", + "description": "The citation type of the reference." + }, + "id": { + "type": ["string", "number"], + "title": "Citation ID", + "description": "The citation identifier (PubMed ID, ISBN, DOI, URL) of the reference." + }, + "name": { + "type": "string", + "title": "Citation Name", + "description": "The citation name of the reference." + }, + "date": { + "type": "string", + "title": "Citation Date", + "description": "The citation date of the reference." + }, + "authors": { + "type": "array", + "title": "Citation Authors", + "description": "The citation authors of the reference.", + "items": { + "type": "string", + "minItems": 0 + } + }, + "comment": { + "type": "string", + "title": "Citation Comment", + "description": "The citation comment of the reference." + } + } + }, + "experiment_context": { + "type": ["array", "null"], + "title": "Experiment Context", + "description": "An experiment context specifies the experiment's parameters where this interaction was observed.", + "items": { + "oneOf": [ + { + "type": "object", + "required": ["name", "value"], + "properties": { + "name": { + "type": "string", + "title": "Annotation Type", + "description": "Annotation type listing - sourced from the BEL Annotation resource names" + }, + "value": { + "type": ["string", "number", "boolean", "array"], + "title": "Annotations", + "description": "Annotations such as Homo sapiens, cancer, epithelial tissue sourced from the BEL Annotation resources", "items": { - "type": "string", - "minItems": 0 + "type": ["string", "number", "boolean"] } - }, - "comment": { - "type": "string", - "title": "Citation Comment", - "description": "The citation comment of the reference." + } } - } - }, - "biological_context": { - "type": ["array", "null"], - "title": "Biological Context", - "description": "A biological context specifies the experiment's parameters where this interaction was observed.", - "items": { - "oneOf": [ - { - "type": "object", - "required": ["name", "value"], - "properties": { - "name": { - "type": "string", - "title": "Annotation Type", - "description": "Annotation type listing - sourced from the BEL Annotation resource names" - }, - "value": { - "type": ["string", "number", "boolean", "array"], - "title": "Annotations", - "description": "Annotations such as Homo sapiens, cancer, epithelial tissue sourced from the BEL Annotation resources", - "items": { - "type": ["string", "number", "boolean"] - } - } - } - }, - { - "type": "object", - "required": ["uri"], - "title": "Annotation URI(s)", - "description": "URI(s) for Annotations", - "properties": { - "uri": { - "type": ["string", "array"], - "format": "uri", - "items": { - "type": "string", - "format": "uri" - } - } - } + }, + { + "type": "object", + "required": [ "uri" ], + "title": "Annotation URI(s)", + "description": "URI(s) for Annotations", + "properties": { + "uri": { + "type": ["string", "array"], + "format": "uri", + "items": { + "type": "string", + "format": "uri" } - ] - } + } + } + } + ] + } }, "summary_text": { - "type": ["string", "null"], - "title": "Summary Text", - "description": "Abstract from source text to provide support for this evidence" + "type": ["string", "null"], + "title": "Summary Text", + "description": "Abstract from source text to provide support for this evidence" + }, + "references": { + "type": ["array", "null"], + "title": "References", + "description": "The references section identifies annotation and namespace URIs.", + "items": { + "type": "object", + "required": ["keyword", "uri"], + "title": "Reference", + "description": "Reference for either Annotation or Namespace stated in the evidence.", + "properties": { + "keyword": { + "type": "string", + "title": "Keyword", + "description": "Keyword reference used in the annotation or namespace value." + }, + "uri": { + "type": "string", + "format": "uri", + "title": "URI", + "description": "URI that identifies the annotation or namespace classification." + } + } + } }, "metadata": { - "type": ["object", "null"], - "title": "Evidence resource metadata", - "description": "Metadata that describes the evidence resource.", - "additionalProperties": true + "type": ["array", "null"], + "title": "Evidence resource metadata", + "description": "Metadata that describes the evidence resource.", + "items": { + "oneOf": [ + { + "type": "object", + "required": ["name", "value"], + "properties": { + "name": { + "type": "string", + "title": "Annotation Type", + "description": "Annotation type listing - sourced from the BEL Annotation resource names" + }, + "value": { + "type": ["string", "number", "boolean", "array"], + "title": "Annotations", + "description": "Annotations such as Homo sapiens, cancer, epithelial tissue sourced from the BEL Annotation resources", + "items": { + "type": ["string", "number", "boolean"] + } + } + } + }, + { + "type": "object", + "required": ["uri"], + "title": "Annotation URI(s)", + "description": "URI(s) for Annotations", + "properties": { + "uri": { + "type": ["string", "array"], + "format": "uri", + "items": { + "type": "string", + "format": "uri" + } + } + } + } + ] + } } } } diff --git a/lib/evidence/facet_filter.rb b/lib/evidence/facet_filter.rb index 2de3965..0973e04 100644 --- a/lib/evidence/facet_filter.rb +++ b/lib/evidence/facet_filter.rb @@ -5,11 +5,11 @@ module Evidence module FacetFilter EMPTY = [] - EVIDENCE_PARTS = ['citation', 'biological_context', 'metadata'] + EVIDENCE_PARTS = ['citation', 'experiment_context', 'metadata'] def map_evidence_facets(evidence) EVIDENCE_PARTS.reduce([]) { |facets, evidence_part| - part = evidence[evidence_part] + part = evidence.send(evidence_part) facets.concat(self.send(:"map_#{evidence_part}_facets", part)) } end @@ -24,17 +24,17 @@ def map_citation_facets(citation) end end - def map_biological_context_facets(biological_context) - if biological_context - biological_context.flat_map { |annotation| + def map_experiment_context_facets(experiment_context) + if experiment_context + experiment_context.flat_map { |annotation| name = annotation[:name] value = annotation[:value] if value.respond_to?(:each) value.map { |v| - [:biological_context, name, v] + [:experiment_context, name, v] } else - [:biological_context, name, value] + [:experiment_context, name, value] end }.select { |category, name, value| value != nil diff --git a/lib/evidence/mongo.rb b/lib/evidence/mongo.rb index 00edf8d..b07a270 100644 --- a/lib/evidence/mongo.rb +++ b/lib/evidence/mongo.rb @@ -102,13 +102,13 @@ def to_query(filters = []) name = filter['name'] value = filter['value'] - if category == 'biological_context' - context = query_hash.fetch('biological_context', nil) + if category == 'experiment_context' + context = query_hash.fetch('experiment_context', nil) if !context context = { :$all => [] } - query_hash['biological_context'] = context + query_hash['experiment_context'] = context end context[:$all] << { diff --git a/spec/api_spec.rb b/spec/api_spec.rb new file mode 100644 index 0000000..dcaf288 --- /dev/null +++ b/spec/api_spec.rb @@ -0,0 +1,33 @@ +require_relative 'spec_helper' + +describe 'API Spec' do + + subject(:api) { + Hyperclient.new(api_root) + } + + it 'return a HAL API description' do + expect(api._response.status).to eql(HTTP_OK) + expect(api._response[:content_type]).to eql(HAL) + end + + it 'does not contain embedded entities' do + expect(api._embedded.to_a).to be_empty + end + + it 'does contain links' do + expect(api._links.to_a).not_to be_empty + end + + it 'links use the item IANA link relation' do + expect(api._links.to_h.keys).to eql(['item']) + end + + it 'can navigate to "evidence" resource' do + evidence_link = api._links.item.find { |item| + item._url =~ %r{/evidence$} + } + expect(evidence_link._options._response.status).to eq(HTTP_OK) + expect(evidence_link._options._response[:allow]).to eq('OPTIONS,POST,GET') + end +end diff --git a/spec/evidence/annotation_spec.rb b/spec/evidence/annotation_spec.rb new file mode 100644 index 0000000..11068bd --- /dev/null +++ b/spec/evidence/annotation_spec.rb @@ -0,0 +1,120 @@ +require_relative '../spec_helper' +require 'json' + +describe 'API Evidence - Annotations' do + + subject(:evidence_api) { + root_resource(:evidence) + } + + it 'stores name/value annotation as-is (free annotation)' do + example = JSON.load(test_file('annotation.json')) + example['evidence']['experiment_context'] << { + 'name' => 'Species', + 'value' => '9606' + } + + post_and_get(example, '/api/evidence') do |response| + resource = response.body + expect(resource).to include('evidence') + expect(resource['evidence']).to be_an(Array) + expect(resource['evidence'].size).to eql(1) + expect(resource['evidence'].first).to include('experiment_context') + + expect(resource['evidence'].first['experiment_context']).to include( + {'name' => 'Species', 'value' => '9606'} + ) + end + end + + it 'normalizes name/value annotation names (free annotation)' do + example = JSON.load(test_file('annotation.json')) + example['evidence']['experiment_context'] = [ + { + 'name' => 'status_value', + 'value' => '1' + }, + { + 'name' => 'Status-Value', + 'value' => '3' + } + ] + + post_and_get(example, '/api/evidence') do |response| + resource = response.body + expect(resource).to include('evidence') + expect(resource['evidence']).to be_an(Array) + expect(resource['evidence'].size).to eql(1) + expect(resource['evidence'].first).to include('experiment_context') + + expect(resource['evidence'].first['experiment_context']).to include( + {'name' => 'StatusValue', 'value' => '1'}, + {'name' => 'StatusValue', 'value' => '3'} + ) + end + end + + it 'normalizes name/value annotation to URI (structured annotation)' do + example = JSON.load(test_file('annotation.json')) + example['evidence']['experiment_context'] = [ + { + 'name' => 'Taxon', + 'value' => '9606' + } + ] + + post_and_get(example, '/api/evidence') do |response| + resource = response.body + expect(resource).to include('evidence') + expect(resource['evidence']).to be_an(Array) + expect(resource['evidence'].size).to eql(1) + expect(resource['evidence'].first).to include('experiment_context') + + expect(resource['evidence'].first['experiment_context']).to include( + {'uri' => "#{api_root}/annotations/Ncbi Taxonomy/values/Homo sapiens"} + ) + end + end + + it 'normalizes resource URI to equivalent URI (structured annotation)' do + example = JSON.load(test_file('annotation.json')) + example['evidence']['experiment_context'] = [ + { + 'uri' => "#{api_root}/annotations/taxon/values/9606" + } + ] + + post_and_get(example, '/api/evidence') do |response| + resource = response.body + expect(resource).to include('evidence') + expect(resource['evidence']).to be_an(Array) + expect(resource['evidence'].size).to eql(1) + expect(resource['evidence'].first).to include('experiment_context') + + expect(resource['evidence'].first['experiment_context']).to include( + {'uri' => "#{api_root}/annotations/Ncbi Taxonomy/values/Homo sapiens"} + ) + end + end + + it "maps an annotation's RDF URI to an API URI" do + example = JSON.load(test_file('annotation.json')) + example['evidence']['experiment_context'] = [ + { + 'uri' => 'http://www.openbel.org/bel/namespace/ncbi-taxonomy/9606' + } + ] + + post_and_get(example, '/api/evidence') do |response| + resource = response.body + expect(resource).to include('evidence') + expect(resource['evidence']).to be_an(Array) + expect(resource['evidence'].size).to eql(1) + expect(resource['evidence'].first).to include('experiment_context') + + expect(resource['evidence'].first['experiment_context']).to include( + {'uri' => "#{api_root}/annotations/Ncbi Taxonomy/values/Homo sapiens"} + ) + end + end +end diff --git a/spec/evidence/api_evidence_spec.rb b/spec/evidence/api_evidence_spec.rb new file mode 100644 index 0000000..d4f4883 --- /dev/null +++ b/spec/evidence/api_evidence_spec.rb @@ -0,0 +1,73 @@ +require_relative '../spec_helper' + +describe 'API Evidence' do + + subject(:evidence_api) { + root_resource(:evidence) + } + + it 'returns 404 when the resource collection is empty' do + response = api_conn.get '/api/evidence' + expect(response.status).to eql(404) + end + + it 'returns an array when the resource collection is non-empty' do + # create + response = api_conn.post('/api/evidence') { |req| + req.headers['Content-Type'] = 'application/json; charset=utf-8' + req.body = test_file('example_evidence.json').read + } + expect(response.status).to eql(201) + expect(response['Location']).not_to be_empty + location = response['Location'] + + expect(evidence_api._resource['evidence']).to be_an(Array) + expect(evidence_api._resource['evidence'].size).to eql(1) + + # clean up + api_conn.delete location + end + + it 'instances can be retrieved by id' do + # create + response = api_conn.post('/api/evidence') { |req| + req.headers['Content-Type'] = 'application/json; charset=utf-8' + req.body = test_file('example_evidence.json').read + } + expect(response.status).to eql(201) + expect(response['Location']).not_to be_empty + location = response['Location'] + + # retrieve + response = api_conn.get location + expect(response.status).to eql(200) + expect(response['Content-Type']).to match HAL_REGEX + + # clean up + api_conn.delete location + end + + it 'is pageable' do + # XXX Fix paging in /api/evidence; the "start" and "next" link rel + # should be templatable. + + # create + response = api_conn.post('/api/evidence') { |req| + req.headers['Content-Type'] = 'application/json; charset=utf-8' + req.body = test_file('example_evidence.json').read + } + expect(response.status).to eql(201) + expect(response['Location']).not_to be_empty + location = response['Location'] + + evidence_resource = api_conn.get { |req| + req.url '/api/evidence', :start => 0, :size => 1 + }.body + + expect(evidence_resource['evidence']).not_to be_nil + expect(evidence_resource['evidence'].size).to eql(1) + + # clean up + api_conn.delete location + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..343324d --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,81 @@ +require 'rspec' +require 'hyperclient' +require 'json' + +HAL = 'application/hal+json' +HAL_REGEX = Regexp.escape(HAL) +HTTP_OK = 200 + +def api_root + ENV['API_ROOT_URL'] || (raise RuntimeError.new('API_ROOT_URL is not set')) +end + +def root_resource(resource_name) + api_client = Hyperclient.new(api_root) do |c| + c.connection do |conn| + conn.adapter Faraday.default_adapter + #conn.response :logger + conn.response :json, :content_type => 'application/hal+json' + end + end + api_client.headers['Content-Type'] = 'application/json' + resource = api_client._links.item.find { |item| + item._url =~ %r{/#{resource_name}$} + } + + unless resource + msg = "#{resource_name.to_s.capitalize} API _link cannot be found." + raise RuntimeError.new(msg) + end + + resource +end + +def api_conn + Faraday.new(:url => api_root) do |builder| + builder.adapter Faraday.default_adapter + #builder.response :logger + builder.response :json, :content_type => 'application/hal+json' + end +end + +def test_file(name) + File.open( + File.join( + File.dirname(File.expand_path(__FILE__)), + 'test_data', + name + ), + :ext_enc => Encoding::UTF_8 + ) +end + +def post_and_get(content, url) + data = + case + when content.respond_to?(:read) + content.read + when content.respond_to?(:each_pair) + JSON.dump(content) + else + content.to_s + end + + response = api_conn.post(url) { |req| + req.headers['Content-Type'] = 'application/json; charset=utf-8' + req.body = data + } + + location = response['Location'] + response = api_conn.get location + + if block_given? + begin + yield response + ensure + api_conn.delete location + end + else + response + end +end diff --git a/spec/test_data/annotation.json b/spec/test_data/annotation.json new file mode 100644 index 0000000..668fa8a --- /dev/null +++ b/spec/test_data/annotation.json @@ -0,0 +1,18 @@ +{ + "evidence": { + "bel_statement": "biologicalProcess(GOBP:aging) increases biologicalProcess(GOBP:\"apoptotic process\")", + "citation": { + "type": "PubMed", + "name": "Trends in molecular medicine", + "id": "12928037", + "date": "2003-08-09", + "authors": "de Nigris F|Lerman A|Ignarro LJ|Williams-Ignarro S|Sica V|Baker AH|Lerman LO|Geng YJ|Napoli C", + "comment": "Primary literature from (Trends in Molecular Medicine)." + }, + "summary_text": "Aging, one of the major predictors for atherosclerotic lesion formation, increases\nthe sensitivity of endothelial cells to apoptosis induced by in vitro and in vivo\nstimuli [35–37].", + "experiment_context": [ + ], + "metadata": { + } + } +} diff --git a/spec/test_data/example_evidence.json b/spec/test_data/example_evidence.json new file mode 100644 index 0000000..fae82a3 --- /dev/null +++ b/spec/test_data/example_evidence.json @@ -0,0 +1,111 @@ +{ + "evidence": { + "bel_statement": "biologicalProcess(GOBP:aging) increases biologicalProcess(GOBP:\"apoptotic process\")", + "citation": { + "type": "PubMed", + "name": "Trends in molecular medicine", + "id": "12928037", + "date": "2003-08-09", + "authors": "de Nigris F|Lerman A|Ignarro LJ|Williams-Ignarro S|Sica V|Baker AH|Lerman LO|Geng YJ|Napoli C", + "comment": "Primary literature from (Trends in Molecular Medicine)." + }, + "summary_text": "Aging, one of the major predictors for atherosclerotic lesion formation, increases\nthe sensitivity of endothelial cells to apoptosis induced by in vitro and in vivo\nstimuli [35–37].", + "experiment_context": [ + { + "name": "Disease", + "value": "atherosclerosis", + "uri": "http://www.openbel.org/bel/namespace/disease-ontology/atherosclerosis", + "url": "http://www.openbel.org/bel/namespace/disease-ontology" + }, + { + "name": "Anatomy", + "value": "artery", + "uri": "http://www.openbel.org/bel/namespace/uberon/artery", + "url": "http://www.openbel.org/bel/namespace/uberon" + }, + { + "name": "TextLocation", + "value": "Review" + }, + { + "name": "Cell", + "value": "endothelial cell", + "uri": "http://www.openbel.org/bel/namespace/cell-ontology/endothelial cell", + "url": "http://www.openbel.org/bel/namespace/cell-ontology" + }, + { + "name": "MeSHAnatomy", + "value": "Muscle, Smooth, Vascular", + "uri": "http://www.openbel.org/bel/namespace/mesh-anatomy/Muscle, Smooth, Vascular", + "url": "http://www.openbel.org/bel/namespace/mesh-anatomy" + } + ], + "metadata": { + "document_header": { + "Name": "BEL Framework Small Corpus Document", + "Description": "Approximately 2000 hand curated statements drawn from 57 PubMeds.", + "Version": "20131211", + "Copyright": "Copyright (c) 2011-2012, Selventa. All Rights Reserved.", + "Authors": "Selventa", + "Licenses": "Creative Commons Attribution-Non-Commercial-ShareAlike 3.0 Unported License", + "ContactInfo": "support@belframework.org" + }, + "namespace_definitions": { + "CHEBI": "http://www.openbel.org/bel/namespace/chebi", + "CHEBIID": "http://www.openbel.org/bel/namespace/chebi", + "EGID": "http://www.openbel.org/bel/namespace/entrez-gene", + "GOBP": "http://www.openbel.org/bel/namespace/go-biological-process", + "HGNC": "http://www.openbel.org/bel/namespace/hgnc-human-genes", + "MESHCS": "http://www.openbel.org/bel/namespace/mesh-cellular-structures", + "MESHD": "http://www.openbel.org/bel/namespace/mesh-diseases", + "MESHPP": "http://www.openbel.org/bel/namespace/mesh-processes", + "MGI": "http://www.openbel.org/bel/namespace/mgi-mouse-genes", + "RGD": "http://www.openbel.org/bel/namespace/rgd-rat-genes", + "SCHEM": "http://www.openbel.org/bel/namespace/selventa-legacy-chemicals", + "SCOMP": "http://www.openbel.org/bel/namespace/selventa-named-complexes", + "SDIS": "http://www.openbel.org/bel/namespace/selventa-legacy-diseases", + "SFAM": "http://www.openbel.org/bel/namespace/selventa-protein-families", + "SPAC": "http://www.openbel.org/bel/namespace/swissprot" + }, + "annotation_definitions": { + "Anatomy": { + "type": "url", + "domain": "http://www.openbel.org/bel/namespace/uberon" + }, + "Cell": { + "type": "url", + "domain": "http://www.openbel.org/bel/namespace/cell-ontology" + }, + "CellLine": { + "type": "url", + "domain": "http://www.openbel.org/bel/namespace/cell-line-ontology" + }, + "CellStructure": { + "type": "url", + "domain": "http://www.openbel.org/bel/namespace/mesh-cellular-structures" + }, + "Disease": { + "type": "url", + "domain": "http://www.openbel.org/bel/namespace/disease-ontology" + }, + "MeSHAnatomy": { + "type": "url", + "domain": "http://www.openbel.org/bel/namespace/mesh-anatomy" + }, + "Species": { + "type": "url", + "domain": "http://www.openbel.org/bel/namespace/ncbi-taxonomy" + }, + "TextLocation": { + "type": "list", + "domain": [ + "Abstract", + "Results", + "Legend", + "Review" + ] + } + } + } + } +} diff --git a/tools/scripts/api-benchmark.sh b/tools/scripts/api-benchmark.sh index 1bb0b05..dc36c1b 100755 --- a/tools/scripts/api-benchmark.sh +++ b/tools/scripts/api-benchmark.sh @@ -4,9 +4,9 @@ cd "$DIR" || exit 1 . "$DIR"/env.sh || exit 1 # requirements +require_cmd bundle require_cmd curl require_cmd siege -require_cmd pumactl SERVER_AS_DAEMON=1 diff --git a/tools/scripts/api-restart.sh b/tools/scripts/api-restart.sh index 76a71bc..7fe0716 100755 --- a/tools/scripts/api-restart.sh +++ b/tools/scripts/api-restart.sh @@ -5,9 +5,9 @@ cd "$DIR" || exit 1 . "$DIR"/env.sh || exit 1 # requirements -require_cmd pumactl +require_cmd bundle -pumactl \ +bundle exec pumactl \ --config-file "$CONFIGS"/server_config.rb \ restart diff --git a/tools/scripts/api-start.sh b/tools/scripts/api-start.sh index 8786452..b6f5889 100755 --- a/tools/scripts/api-start.sh +++ b/tools/scripts/api-start.sh @@ -5,9 +5,9 @@ cd "$DIR" || exit 1 . "$DIR"/env.sh || exit 1 # requirements -require_cmd pumactl +require_cmd bundle -pumactl \ +bundle exec pumactl \ --config-file "$CONFIGS"/server_config.rb \ start diff --git a/tools/scripts/api-stop.sh b/tools/scripts/api-stop.sh index 8a68766..204664f 100755 --- a/tools/scripts/api-stop.sh +++ b/tools/scripts/api-stop.sh @@ -5,9 +5,9 @@ cd "$DIR" || exit 1 . "$DIR"/env.sh || exit 1 # requirements -require_cmd pumactl +require_cmd bundle -pumactl \ +bundle exec pumactl \ --config-file "$CONFIGS"/server_config.rb \ stop