openbel-api 0.5.1-java → 0.6.1-java
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gemspec +14 -6
- data/CHANGELOG.md +32 -0
- data/README.md +2 -2
- data/VERSION +1 -1
- data/app/openbel/api/config.rb +12 -0
- data/app/openbel/api/helpers/base.rb +17 -0
- data/app/openbel/api/helpers/evidence.rb +74 -0
- data/app/openbel/api/helpers/filters.rb +94 -0
- data/app/openbel/api/helpers/translators.rb +73 -0
- data/app/openbel/api/resources/evidence_transform.rb +1 -13
- data/app/openbel/api/routes/authenticate.rb +0 -92
- data/app/openbel/api/routes/base.rb +19 -5
- data/app/openbel/api/routes/datasets.rb +65 -151
- data/app/openbel/api/routes/evidence.rb +10 -95
- data/config/config.yml +4 -6
- data/lib/openbel/api/evidence/api.rb +8 -0
- data/lib/openbel/api/evidence/facet_filter.rb +3 -3
- data/lib/openbel/api/evidence/mongo.rb +304 -44
- data/lib/openbel/api/evidence/mongo_facet.rb +173 -66
- metadata +112 -26
- data/config/async_evidence.rb +0 -12
- data/config/async_jena.rb +0 -14
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1fa47f8991f91aad490818b0c6e6a5fc8f822ecc
|
4
|
+
data.tar.gz: bbd5f02dfa6c64f060181b029d138960a9ef691f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d2fe0173aad3c83200e69361bcd632ee38d19d6b52465f63678fd3385f792cfc491841bee631f4229289e43e843ac3e8e05dddec99d8a64f35508775574a390d
|
7
|
+
data.tar.gz: 28c72b97ae6f3eb3381f51a4194a16b84760549a07c3bcfb31ca7c9a05bfb824f55418d5d3f9fd4017e0d63df2d119a05a27856b6cd4b192909bdfe1a5a9bf21
|
data/.gemspec
CHANGED
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
|
|
11
11
|
'Nick Bargnesi',
|
12
12
|
'William Hayes'
|
13
13
|
]
|
14
|
-
spec.date = %q{
|
14
|
+
spec.date = %q{2016-03-16}
|
15
15
|
spec.email = %q{abargnesi@selventa.com}
|
16
16
|
spec.files = [
|
17
17
|
Dir.glob('app/**/*.{json,rb,ru}'),
|
@@ -34,16 +34,25 @@ Gem::Specification.new do |spec|
|
|
34
34
|
# Dependencies
|
35
35
|
|
36
36
|
## bel.rb
|
37
|
-
spec.add_runtime_dependency 'bel', '0.
|
37
|
+
spec.add_runtime_dependency 'bel', '0.6.0'
|
38
|
+
|
39
|
+
## bel.rb translator dependencies
|
40
|
+
spec.add_runtime_dependency 'json-ld', '1.99.0'
|
41
|
+
spec.add_runtime_dependency 'rdf-json', '1.99.0'
|
42
|
+
spec.add_runtime_dependency 'rdf-rdfa', '1.99.0'
|
43
|
+
spec.add_runtime_dependency 'rdf-rdfxml', '1.99.0'
|
44
|
+
spec.add_runtime_dependency 'rdf-trig', '1.99.0.1'
|
45
|
+
spec.add_runtime_dependency 'rdf-trix', '1.99.0'
|
46
|
+
spec.add_runtime_dependency 'rdf-turtle', '1.99.0'
|
38
47
|
|
39
48
|
## bel.rb plugin - annotation/namespace search
|
40
49
|
spec.add_runtime_dependency 'bel-search-sqlite', '0.4.2'
|
41
50
|
|
42
51
|
## bel.rb plugin - RDF repository using Apache Jena
|
43
|
-
spec.add_runtime_dependency 'bel-rdf-jena', '0.4.
|
52
|
+
spec.add_runtime_dependency 'bel-rdf-jena', '0.4.2'
|
44
53
|
|
45
54
|
## RDF - RDF abstraction
|
46
|
-
spec.add_runtime_dependency 'rdf', '1.99.
|
55
|
+
spec.add_runtime_dependency 'rdf', '1.99.1'
|
47
56
|
|
48
57
|
## Mongo - Faceted search of evidence.
|
49
58
|
spec.add_runtime_dependency 'mongo', '1.12.5'
|
@@ -55,14 +64,13 @@ Gem::Specification.new do |spec|
|
|
55
64
|
spec.add_runtime_dependency 'json_schema', '0.10.0'
|
56
65
|
spec.add_runtime_dependency 'multi_json', '1.11.2'
|
57
66
|
spec.add_runtime_dependency 'oat', '0.4.6'
|
58
|
-
spec.add_runtime_dependency 'puma', '
|
67
|
+
spec.add_runtime_dependency 'puma', '3.1.0'
|
59
68
|
spec.add_runtime_dependency 'rack', '1.6.4'
|
60
69
|
spec.add_runtime_dependency 'rack-cors', '0.4.0'
|
61
70
|
spec.add_runtime_dependency 'rack-handlers', '0.7.0'
|
62
71
|
spec.add_runtime_dependency 'sinatra', '1.4.6'
|
63
72
|
spec.add_runtime_dependency 'sinatra-contrib', '1.4.6'
|
64
73
|
spec.add_runtime_dependency 'jwt', '1.5.2'
|
65
|
-
spec.add_runtime_dependency 'rest-client', '1.8.0'
|
66
74
|
end
|
67
75
|
# vim: ts=2 sw=2:
|
68
76
|
# encoding: utf-8
|
data/CHANGELOG.md
CHANGED
@@ -3,10 +3,36 @@ All notable changes to openbel-api will be documented in this file. The curated
|
|
3
3
|
|
4
4
|
This project adheres to [Semantic Versioning][Semantic Versioning].
|
5
5
|
|
6
|
+
## [0.6.1][0.6.1] - 2016-03-16
|
7
|
+
### Changed
|
8
|
+
- Bumped gems specification date. Requires new version because 0.6.0 was yanked from RubyGems.
|
9
|
+
|
10
|
+
## [0.6.0][0.6.0] - 2016-03-16
|
11
|
+
### Added
|
12
|
+
- Retrieve evidence in a format supported by BEL translator plugins ([Issue 44][44]).
|
13
|
+
- Retrieve dataset evidence in a format supported by BEL translator plugins ([Issue 99][99]).
|
14
|
+
|
15
|
+
### Fixed
|
16
|
+
- Dataset evidence collection is missing annotation/namespace URIs ([Issue 95][95]).
|
17
|
+
- Facets are not created for evidence uploaded through a dataset.
|
18
|
+
|
19
|
+
### Changed
|
20
|
+
- MongoDB version 3.2.0 is now required due to use of `$slice` operator in Aggregation queries.
|
21
|
+
|
22
|
+
### Known Issue
|
23
|
+
Datasets are stored with a URI computed from the scheme and host that is serving the OpenBEL API. For example if OpenBEL API is served from `http://web.site.com` then dataset URIs will be of the form `http://web.site.com/api/datasets/{UUID}`. If you change the scheme and host these URIs will be inconsistent and you will not be able to retrieve your datasets. You will have to re-import your documents.
|
24
|
+
|
25
|
+
- See [Issue #102][102].
|
26
|
+
|
27
|
+
-----
|
28
|
+
|
6
29
|
## [0.5.1][0.5.1] - 2015-12-18
|
7
30
|
### Fixed
|
8
31
|
- Authentication error for MongoDB user when faceting on `GET /api/evidence` ([Issue #93][93]).
|
9
32
|
|
33
|
+
### Changed
|
34
|
+
- MongoDB version 3.2.0 is now required due to use of `$slice` operator in Aggregation queries ([Issue ?][]).
|
35
|
+
|
10
36
|
-----
|
11
37
|
|
12
38
|
## [0.5.0][0.5.0] - 2015-12-17
|
@@ -40,10 +66,16 @@ This project adheres to [Semantic Versioning][Semantic Versioning].
|
|
40
66
|
- Retrieve equivalent namespace values from the individual.
|
41
67
|
- Retrieve orthologous namespace values from the individual.
|
42
68
|
|
69
|
+
[0.6.1]: https://github.com/OpenBEL/openbel-api/compare/0.6.0...0.6.1
|
70
|
+
[0.6.0]: https://github.com/OpenBEL/openbel-api/compare/0.5.1...0.6.0
|
43
71
|
[0.5.1]: https://github.com/OpenBEL/openbel-api/compare/0.5.0...0.5.1
|
44
72
|
[0.5.0]: https://github.com/OpenBEL/openbel-api/compare/0.4.0...0.5.0
|
45
73
|
[Semantic Versioning]: http://semver.org
|
46
74
|
[MongoDB User Authentication]: https://github.com/OpenBEL/openbel-api/wiki/Configuring-the-Evidence-Store#mongodb-user-authentication
|
75
|
+
[44]: https://github.com/OpenBEL/openbel-api/issues/44
|
47
76
|
[91]: https://github.com/OpenBEL/openbel-api/issues/91
|
48
77
|
[92]: https://github.com/OpenBEL/openbel-api/issues/92
|
49
78
|
[93]: https://github.com/OpenBEL/openbel-api/issues/93
|
79
|
+
[95]: https://github.com/OpenBEL/openbel-api/issues/95
|
80
|
+
[99]: https://github.com/OpenBEL/openbel-api/issues/99
|
81
|
+
[102]: https://github.com/OpenBEL/openbel-api/issues/102
|
data/README.md
CHANGED
@@ -54,7 +54,7 @@ The OpenBEL API is built to run with [JRuby][JRuby] and [Java 8][Java 8].
|
|
54
54
|
- [JRuby][JRuby], 9.x series (9.0.x.0 is recommended)
|
55
55
|
- The 9.x series is required due to a Ruby language 2.0 requirement.
|
56
56
|
- See "Installation" below for configuring JRuby and isolating the openbel-api application.
|
57
|
-
- [MongoDB][MongoDB], version 3.
|
57
|
+
- [MongoDB][MongoDB], version 3.2 or greater
|
58
58
|
- Follow [MongoDB download][MongoDB download] page for download and installation instructions.
|
59
59
|
- [SQLite][SQLite], version 3.8.0 or greater
|
60
60
|
- Follow [SQLite download][SQLite download] page for download and installation instructions.
|
@@ -236,7 +236,7 @@ API documentation with *Try it* functionality is available [here][OpenBEL API do
|
|
236
236
|
|
237
237
|
-----
|
238
238
|
|
239
|
-
Built with collaboration and :heart: by the [OpenBEL][OpenBEL] community.
|
239
|
+
Built with collaboration and a lot of :heart: by the [OpenBEL][OpenBEL] community.
|
240
240
|
|
241
241
|
[OpenBEL]: http://www.openbel.org
|
242
242
|
[OpenBEL Platform]: https://github.com/OpenBEL/openbel-platform
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.6.1
|
data/app/openbel/api/config.rb
CHANGED
@@ -92,6 +92,18 @@ module OpenBEL
|
|
92
92
|
]
|
93
93
|
end
|
94
94
|
|
95
|
+
# Check Mongo server version >= 3.2.
|
96
|
+
# The aggregation framework's $slice operator is used which requires 3.2.
|
97
|
+
if mongo_client.server_version.to_s !~ /^3.2/
|
98
|
+
return [
|
99
|
+
true, <<-ERR
|
100
|
+
MongoDB version 3.2 or greater is required.
|
101
|
+
|
102
|
+
MongoDB version: #{mongo_client.server_version}
|
103
|
+
ERR
|
104
|
+
]
|
105
|
+
end
|
106
|
+
|
95
107
|
# Attempt access of database.
|
96
108
|
db = mongo_client.db(mongo[:database])
|
97
109
|
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module OpenBEL
|
2
|
+
module Helpers
|
3
|
+
|
4
|
+
DEFAULT_CONTENT_TYPE = 'application/hal+json'
|
5
|
+
DEFAULT_CONTENT_TYPE_ID = :hal
|
6
|
+
|
7
|
+
def wants_default?
|
8
|
+
if params[:format]
|
9
|
+
return params[:format] == DEFAULT_CONTENT_TYPE
|
10
|
+
end
|
11
|
+
|
12
|
+
request.accept.any? { |accept_entry|
|
13
|
+
accept_entry.to_s == DEFAULT_CONTENT_TYPE
|
14
|
+
}
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'bel/util'
|
2
|
+
require_relative 'base'
|
3
|
+
require_relative 'translators'
|
4
|
+
|
5
|
+
module OpenBEL
|
6
|
+
module Helpers
|
7
|
+
|
8
|
+
def render_evidence_collection(
|
9
|
+
name, page_results, start, size, filters,
|
10
|
+
filtered_total, collection_total, evidence_api
|
11
|
+
)
|
12
|
+
# see if the user requested a BEL translator (Accept header or ?format)
|
13
|
+
translator = Translators.requested_translator(request, params)
|
14
|
+
translator_plugin = Translators.requested_translator_plugin(request, params)
|
15
|
+
|
16
|
+
halt 404 unless page_results[:cursor].has_next?
|
17
|
+
|
18
|
+
# Serialize to HAL if they [Accept]ed it, specified it as ?format, or
|
19
|
+
# no translator was found to match request.
|
20
|
+
if wants_default? || !translator
|
21
|
+
facets = page_results[:facets]
|
22
|
+
pager = Pager.new(start, size, filtered_total)
|
23
|
+
evidence = page_results[:cursor].map { |item|
|
24
|
+
item.delete('facets')
|
25
|
+
item
|
26
|
+
}.to_a
|
27
|
+
|
28
|
+
options = {
|
29
|
+
:facets => facets,
|
30
|
+
:start => start,
|
31
|
+
:size => size,
|
32
|
+
:filters => filters,
|
33
|
+
:metadata => {
|
34
|
+
:collection_paging => {
|
35
|
+
:total => collection_total,
|
36
|
+
:total_filtered => pager.total_size,
|
37
|
+
:total_pages => pager.total_pages,
|
38
|
+
:current_page => pager.current_page,
|
39
|
+
:current_page_size => evidence.size,
|
40
|
+
}
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
44
|
+
# pager links
|
45
|
+
options[:previous_page] = pager.previous_page
|
46
|
+
options[:next_page] = pager.next_page
|
47
|
+
|
48
|
+
render_collection(evidence, :evidence, options)
|
49
|
+
else
|
50
|
+
extension = translator_plugin.file_extensions.first
|
51
|
+
|
52
|
+
response.headers['Content-Type'] = translator_plugin.media_types.first
|
53
|
+
status 200
|
54
|
+
attachment "#{name}.#{extension}"
|
55
|
+
stream :keep_open do |response|
|
56
|
+
cursor = page_results[:cursor]
|
57
|
+
dataset_evidence = cursor.lazy.map { |evidence|
|
58
|
+
evidence.delete('facets')
|
59
|
+
evidence.delete('_id')
|
60
|
+
evidence = BEL::Model::Evidence.create(BEL.keys_to_symbols(evidence))
|
61
|
+
evidence.bel_statement = BEL::Model::Evidence.parse_statement(evidence)
|
62
|
+
evidence
|
63
|
+
}
|
64
|
+
|
65
|
+
translator.write(
|
66
|
+
dataset_evidence, response,
|
67
|
+
:annotation_reference_map => evidence_api.find_all_annotation_references,
|
68
|
+
:namespace_reference_map => evidence_api.find_all_namespace_references
|
69
|
+
)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module OpenBEL
|
2
|
+
module Helpers
|
3
|
+
|
4
|
+
# Parse filter query parameters and partition into an {Array}. The first
|
5
|
+
# index will contain the valid filters and the second index will contain
|
6
|
+
# the invalid filters.
|
7
|
+
#
|
8
|
+
# @param [Array<String>] filter_query_params an array of filter strings
|
9
|
+
# encoded in JSON
|
10
|
+
# @return [Array<Array<Hash>, Array<String>] the first index holds the
|
11
|
+
# valid, filter {Hash hashes}; the second index holds the invalid,
|
12
|
+
# filter {String strings}
|
13
|
+
def parse_filters(filter_query_params)
|
14
|
+
filter_query_params.map { |filter_string|
|
15
|
+
begin
|
16
|
+
MultiJson.load filter_string
|
17
|
+
rescue MultiJson::ParseError => ex
|
18
|
+
"#{ex} (filter: #{filter_string})"
|
19
|
+
end
|
20
|
+
}.partition { |filter|
|
21
|
+
filter.is_a?(Hash)
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
# Retrieve the filters that do not provide category, name, and value keys.
|
26
|
+
#
|
27
|
+
# The parsed, incomplete filters will contain an +:error+ key that provides
|
28
|
+
# an error message intended for the user.
|
29
|
+
#
|
30
|
+
# @param [Array<Hash>] filters an array of filter {Hash hashes}
|
31
|
+
# @return [Array<Hash>] an array of incomplete filter {Hash hashes} that
|
32
|
+
# contain a human-readable error at the +:error+ key
|
33
|
+
def incomplete_filters(filters)
|
34
|
+
filters.select { |filter|
|
35
|
+
['category', 'name', 'value'].any? { |f| !filter.include? f }
|
36
|
+
}.map { |incomplete_filter|
|
37
|
+
category, name, value = incomplete_filter.values_at('category', 'name', 'value')
|
38
|
+
error = <<-MSG.gsub(/^\s+/, '').strip
|
39
|
+
Incomplete filter, category:"#{category}", name:"#{name}", and value:"#{value}".
|
40
|
+
MSG
|
41
|
+
incomplete_filter.merge(:error => error)
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
# Retrieve the filters that represent invalid full-text search values.
|
46
|
+
#
|
47
|
+
# The parsed, invalid full-text search filters will contain an +:error+ key
|
48
|
+
# that provides an error message intended for the user.
|
49
|
+
#
|
50
|
+
# @param [Array<Hash>] filters an array of filter {Hash hashes}
|
51
|
+
# @return [Array<Hash>] an array of invalid full-text search filter
|
52
|
+
# {Hash hashes} that contain a human-readable error at the
|
53
|
+
# +:error+ key
|
54
|
+
def invalid_fts_filters(filters)
|
55
|
+
filters.select { |filter|
|
56
|
+
category, name, value = filter.values_at('category', 'name', 'value')
|
57
|
+
category == 'fts' && name == 'search' && value.to_s.length <= 1
|
58
|
+
}.map { |invalid_fts_filter|
|
59
|
+
error = <<-MSG.gsub(/^\s+/, '').strip
|
60
|
+
Full-text search filter values must be larger than one.
|
61
|
+
MSG
|
62
|
+
invalid_fts_filter.merge(:error => error)
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
# Validate the requested filter query strings. If all filters are valid
|
67
|
+
# then return them as {Hash hashes}, otherwise halt 400 Bad Request and
|
68
|
+
# return JSON error response.
|
69
|
+
def validate_filters!
|
70
|
+
filter_query_params = CGI::parse(env["QUERY_STRING"])['filter']
|
71
|
+
valid_filters, invalid_filters = parse_filters(filter_query_params)
|
72
|
+
|
73
|
+
invalid_filters |= incomplete_filters(valid_filters)
|
74
|
+
invalid_filters |= invalid_fts_filters(valid_filters)
|
75
|
+
|
76
|
+
return valid_filters if invalid_filters.empty?
|
77
|
+
|
78
|
+
halt(400, { 'Content-Type' => 'application/json' }, render_json({
|
79
|
+
:status => 400,
|
80
|
+
:msg => "Bad Request",
|
81
|
+
:detail =>
|
82
|
+
invalid_filters.
|
83
|
+
map { |invalid_filter|
|
84
|
+
if invalid_filter.is_a?(Hash) && invalid_filter[:error]
|
85
|
+
invalid_filter[:error]
|
86
|
+
else
|
87
|
+
invalid_filter
|
88
|
+
end
|
89
|
+
}.
|
90
|
+
map(&:to_s)
|
91
|
+
}))
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'bel'
|
2
|
+
|
3
|
+
module OpenBEL
|
4
|
+
module Helpers
|
5
|
+
|
6
|
+
# Helpers for translator functionality based on user's requested media
|
7
|
+
# type.
|
8
|
+
module Translators
|
9
|
+
|
10
|
+
# Open {::Sinatra::Helpers::Stream} and add the +puts+, +write+, and
|
11
|
+
# +flush+ methods. This is necessary because the RDF.rb writers will call
|
12
|
+
# these methods on the IO (in this case {::Sinatra::Helpers::Stream}).
|
13
|
+
class ::Sinatra::Helpers::Stream
|
14
|
+
|
15
|
+
# Write each string in +args*, new-line delimited, to the stream.
|
16
|
+
def puts(*args)
|
17
|
+
self << (
|
18
|
+
args.map { |string| "#{string.encode(Encoding::UTF_8)}\n" }.join
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Write the string to the stream.
|
23
|
+
def write(string)
|
24
|
+
self << string.encode(Encoding::UTF_8)
|
25
|
+
end
|
26
|
+
|
27
|
+
# flush is a no-op; flushing is handled by sinatra/rack server
|
28
|
+
def flush; end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Find a bel.rb translator plugin by value. The value is commonly the
|
32
|
+
# id, file extension, or media type associated with the translator
|
33
|
+
# plugin.
|
34
|
+
#
|
35
|
+
# @param [#to_s] value used to look up translator plugin registered
|
36
|
+
# with bel.rb
|
37
|
+
# @return [BEL::Translator] the translator instance; or +nil+ if one
|
38
|
+
# cannot be found
|
39
|
+
def self.for(value)
|
40
|
+
BEL.translator(symbolize_value(value))
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.plugin_for(value)
|
44
|
+
BEL::Translator::Plugins.for(symbolize_value(value))
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.requested_translator_plugin(request, params)
|
48
|
+
if params && params[:format]
|
49
|
+
self.plugin_for(params[:format])
|
50
|
+
else
|
51
|
+
request.accept.flat_map { |accept_entry|
|
52
|
+
self.plugin_for(accept_entry)
|
53
|
+
}.compact.first
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.requested_translator(request, params)
|
58
|
+
if params && params[:format]
|
59
|
+
self.for(params[:format])
|
60
|
+
else
|
61
|
+
request.accept.flat_map { |accept_entry|
|
62
|
+
self.for(accept_entry)
|
63
|
+
}.compact.first
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.symbolize_value(value)
|
68
|
+
value.to_s.to_sym
|
69
|
+
end
|
70
|
+
private_class_method :symbolize_value
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -94,23 +94,11 @@ module OpenBEL
|
|
94
94
|
|
95
95
|
def free_annotation(name, value)
|
96
96
|
{
|
97
|
-
:name =>
|
97
|
+
:name => name,
|
98
98
|
:value => value
|
99
99
|
}
|
100
100
|
end
|
101
101
|
|
102
|
-
def normalize_annotation_name(name, options = {})
|
103
|
-
name_s = name.to_s
|
104
|
-
|
105
|
-
if name_s.empty?
|
106
|
-
nil
|
107
|
-
else
|
108
|
-
name_s.
|
109
|
-
split(%r{[^a-zA-Z0-9]+}).
|
110
|
-
map! { |word| word.capitalize }.
|
111
|
-
join
|
112
|
-
end
|
113
|
-
end
|
114
102
|
end
|
115
103
|
|
116
104
|
class AnnotationGroupingTransform
|