Module: OpenBEL::Helpers

Included in:
Routes::Datasets, Routes::Nanopubs
Defined in:
app/openbel/api/helpers/base.rb,
app/openbel/api/helpers/pager.rb,
app/openbel/api/helpers/filters.rb,
app/openbel/api/helpers/nanopub.rb,
app/openbel/api/helpers/translators.rb,
lib/openbel/api/helpers/uuid_generator.rb,
lib/openbel/api/helpers/dependency_graph.rb

Defined Under Namespace

Modules: Translators, UUIDGenerator Classes: DependencyGraph, Pager

Constant Summary collapse

DEFAULT_CONTENT_TYPE =
'application/hal+json'
DEFAULT_CONTENT_TYPE_ID =
:hal

Instance Method Summary collapse

Instance Method Details

#incomplete_filters(filters) ⇒ Array<Hash>

Retrieve the filters that do not provide category, name, and value keys.

The parsed, incomplete filters will contain an :error key that provides an error message intended for the user.

Parameters:

  • filters (Array<Hash>)

    an array of filter hashes

Returns:

  • (Array<Hash>)

    an array of incomplete filter hashes that contain a human-readable error at the :error key



33
34
35
36
37
38
39
40
41
42
43
# File 'app/openbel/api/helpers/filters.rb', line 33

def incomplete_filters(filters)
  filters.select { |filter|
    ['category', 'name', 'value'].any? { |f| !filter.include? f }
  }.map { |incomplete_filter|
    category, name, value = incomplete_filter.values_at('category', 'name', 'value')
    error = <<-MSG.gsub(/^\s+/, '').strip
      Incomplete filter, category:"#{category}", name:"#{name}", and value:"#{value}".
    MSG
    incomplete_filter.merge(:error => error)
  }
end

#invalid_fts_filters(filters) ⇒ Array<Hash>

Retrieve the filters that represent invalid full-text search values.

The parsed, invalid full-text search filters will contain an :error key that provides an error message intended for the user.

Parameters:

  • filters (Array<Hash>)

    an array of filter hashes

Returns:

  • (Array<Hash>)

    an array of invalid full-text search filter hashes that contain a human-readable error at the :error key



54
55
56
57
58
59
60
61
62
63
64
# File 'app/openbel/api/helpers/filters.rb', line 54

def invalid_fts_filters(filters)
  filters.select { |filter|
    category, name, value = filter.values_at('category', 'name', 'value')
    category == 'fts' && name == 'search' && value.to_s.length <= 1
  }.map { |invalid_fts_filter|
    error = <<-MSG.gsub(/^\s+/, '').strip
      Full-text search filter values must be larger than one.
    MSG
    invalid_fts_filter.merge(:error => error)
  }
end

#parse_filters(filter_query_params) ⇒ Array<Array<Hash>, Array<String>] the first index holds the valid, ...

Parse filter query parameters and partition into an Array. The first index will contain the valid filters and the second index will contain the invalid filters.

Parameters:

  • filter_query_params (Array<String>)

    an array of filter strings encoded in JSON

Returns:

  • (Array<Array<Hash>, Array<String>] the first index holds the valid, filter {Hash hashes}; the second index holds the invalid, filter {String strings})

    Array<Array<Hash>, Array<String>] the first index holds the valid, filter hashes; the second index holds the invalid, filter strings



13
14
15
16
17
18
19
20
21
22
23
# File 'app/openbel/api/helpers/filters.rb', line 13

def parse_filters(filter_query_params)
  filter_query_params.map { |filter_string|
    begin
      MultiJson.load filter_string
    rescue MultiJson::ParseError => ex
      "#{ex} (filter: #{filter_string})"
    end
  }.partition { |filter|
    filter.is_a?(Hash)
  }
end

#render_nanopub_collection(name, page_results, start, size, filters, filtered_total, collection_total, nanopub_api) ⇒ Object



8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'app/openbel/api/helpers/nanopub.rb', line 8

def render_nanopub_collection(
  name, page_results, start, size, filters,
  filtered_total, collection_total, nanopub_api
)
  # see if the user requested a BEL translator (Accept header or ?format)
  translator        = Translators.requested_translator(request, params)
  translator_plugin = Translators.requested_translator_plugin(request, params)

  halt 404 unless page_results[:cursor].has_next?

  # Serialize to HAL if they [Accept]ed it, specified it as ?format, or
  # no translator was found to match request.
  if wants_default? || !translator
    facets   = page_results[:facets]
    pager    = Pager.new(start, size, filtered_total)
    nanopub = page_results[:cursor].map { |item|
                 item.delete('facets')
                 item
               }.to_a

    options = {
      :facets   => facets,
      :start    => start,
      :size     => size,
      :filters  => filters,
      :metadata => {
        :collection_paging => {
          :total                  => collection_total,
          :total_filtered         => pager.total_size,
          :total_pages            => pager.total_pages,
          :current_page           => pager.current_page,
          :current_page_size      => nanopub.size,
        }
      }
    }

    # pager links
    options[:previous_page] = pager.previous_page
    options[:next_page]     = pager.next_page

    render_collection(nanopub, :nanopub, options)
  else
    extension = translator_plugin.file_extensions.first

    response.headers['Content-Type'] = translator_plugin.media_types.first
    status 200
    attachment "#{name}.#{extension}"
    stream :keep_open do |response|
      cursor             = page_results[:cursor]
      dataset_nanopub = cursor.lazy.map { |nanopub|
        nanopub.delete('facets')
        nanopub.delete('_id')
        nanopub = BEL::Nanopub::Nanopub.create(BEL.keys_to_symbols(nanopub))
        nanopub
      }

      translator.write(
        dataset_nanopub, response,
        :annotation_reference_map => nanopub_api.find_all_annotation_references,
        :namespace_reference_map  => nanopub_api.find_all_namespace_references
      )
    end
  end
end

#validate_experiment_context(experiment_context) ⇒ Object



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'app/openbel/api/helpers/nanopub.rb', line 73

def validate_experiment_context(experiment_context)
  valid_annotations   = []
  invalid_annotations = []
  experiment_context.values.each do |annotation|
    name, value = annotation.values_at(:name, :value)
    found_annotation  = @annotations.find(name).first

    if found_annotation
      if found_annotation.find(value).first == nil
        # structured annotations, without a match, is invalid
        invalid_annotations << annotation
      else
        # structured annotations, with a match, is invalid
        valid_annotations << annotation
      end
    else
      # free annotations considered valid
      valid_annotations << annotation
    end
  end

  [
    invalid_annotations.empty? ? :valid : :invalid,
    {
      :valid               => invalid_annotations.empty?,
      :valid_annotations   => valid_annotations,
      :invalid_annotations => invalid_annotations,
      :message             =>
        invalid_annotations
          .map { |annotation|
            name, value = annotation.values_at(:name, :value)
            %Q{The value "#{value}" was not found in annotation "#{name}".}
          }
          .join("\n")
    }
  ]
end

#validate_filters!Object

Validate the requested filter query strings. If all filters are valid then return them as hashes, otherwise halt 400 Bad Request and return JSON error response.



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'app/openbel/api/helpers/filters.rb', line 69

def validate_filters!
  filter_query_params = CGI::parse(env["QUERY_STRING"])['filter']
  valid_filters, invalid_filters = parse_filters(filter_query_params)

  invalid_filters |= incomplete_filters(valid_filters)
  invalid_filters |= invalid_fts_filters(valid_filters)

  return valid_filters if invalid_filters.empty?

  halt(400, { 'Content-Type' => 'application/json' }, render_json({
    :status => 400,
    :msg => "Bad Request",
    :detail =>
      invalid_filters.
      map { |invalid_filter|
        if invalid_filter.is_a?(Hash) && invalid_filter[:error]
          invalid_filter[:error]
        else
          invalid_filter
        end
      }.
      map(&:to_s)
  }))
end

#validate_nanopub!(bel_statement, experiment_context) ⇒ Object



212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'app/openbel/api/helpers/nanopub.rb', line 212

def validate_nanopub!(bel_statement, experiment_context)
  stmt_result, stmt_validation     = validate_statement(bel_statement)
  expctx_result, expctx_validation = validate_experiment_context(experiment_context)

  return nil if stmt_result == :valid && expctx_result == :valid

  halt(
    422,
    {'Content-Type' => 'application/json'},
    render_json(
      {
        :nanopub_validation => {
          :bel_statement_validation      => stmt_validation,
          :experiment_context_validation => expctx_validation
        }
      }
    )
  )
end

#validate_statement(bel) ⇒ Object



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'app/openbel/api/helpers/nanopub.rb', line 111

def validate_statement(bel)
  filter =
    BELParser::ASTFilter.new(
      BELParser::ASTGenerator.new("#{bel}\n"),
      :simple_statement,
      :observed_term,
      :nested_statement
    )
  _, _, ast = filter.each.first

  if ast.nil? || ast.empty?
    return [
      :syntax_invalid,
      {
        valid_syntax:    false,
        valid_semantics: false,
        message:         'Invalid syntax.',
        warnings:        [],
        term_signatures: []
      }
    ]
  end

  urir      = BELParser::Resource.default_uri_reader
  urlr      = BELParser::Resource.default_url_reader
  validator = BELParser::Language::ExpressionValidator.new(@spec, @supported_namespaces, urir, urlr)
  message   = ''
  terms     = ast.first.traverse.select { |node| node.type == :term }.to_a

  semantics_functions =
    BELParser::Language::Semantics.semantics_functions.reject { |fun|
      fun == BELParser::Language::Semantics::SignatureMapping
    }

  result        = validator.validate(ast.first)
  syntax_errors = result.syntax_results.map(&:to_s)

  semantic_warnings =
    ast
      .first
      .traverse
      .flat_map { |node|
        semantics_functions.flat_map { |func|
          func.map(node, @spec, @supported_namespaces)
        }
      }
      .compact

  if syntax_errors.empty? && semantic_warnings.empty?
    valid = true
  else
    valid   = false
    message = ''
    message +=
      syntax_errors.reduce('') { |msg, error|
        msg << "#{error}\n"
      }
    message +=
      semantic_warnings.reduce('') { |msg, warning|
        msg << "#{warning}\n"
      }
    message << "\n"
  end

  term_semantics =
    terms.map { |term|
      term_result = validator.validate(term)
      valid      &= term_result.valid_semantics?
      bel_term    = serialize(term)

      unless valid
        message << "Term: #{bel_term}\n"
        term_result.invalid_signature_mappings.map { |m|
          message << "  #{m}\n"
        }
        message << "\n"
      end

      {
        term:               bel_term,
        valid:              term_result.valid_semantics?,
        errors:             term_result.syntax_results.map(&:to_s),
        valid_signatures:   term_result.valid_signature_mappings.map(&:to_s),
        invalid_signatures: term_result.invalid_signature_mappings.map(&:to_s)
      }
    }

  [
    valid ? :valid : :semantics_invalid,
    {
      expression:      bel,
      valid_syntax:    true,
      valid_semantics: valid,
      message:         valid ? 'Valid semantics' : message,
      errors:          syntax_errors,
      warnings:        semantic_warnings.map(&:to_s),
      term_signatures: term_semantics
    }
  ]
end

#wants_default?Boolean

Returns:

  • (Boolean)


7
8
9
10
11
12
13
14
15
# File 'app/openbel/api/helpers/base.rb', line 7

def wants_default?
  if params[:format]
    return params[:format] == DEFAULT_CONTENT_TYPE
  end

  request.accept.any? { |accept_entry|
    accept_entry.to_s == DEFAULT_CONTENT_TYPE
  }
end