cff 0.8.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/cff/entity.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright (c) 2018-2021 The Ruby Citation File Format Developers.
3
+ # Copyright (c) 2018-2022 The Ruby Citation File Format Developers.
4
4
  #
5
5
  # Licensed under the Apache License, Version 2.0 (the "License");
6
6
  # you may not use this file except in compliance with the License.
@@ -14,9 +14,11 @@
14
14
  # See the License for the specific language governing permissions and
15
15
  # limitations under the License.
16
16
 
17
+ require_relative 'model_part'
18
+ require_relative 'schema'
19
+
17
20
  ##
18
21
  module CFF
19
-
20
22
  # An Entity can represent different types of entities, e.g., a publishing
21
23
  # company, or conference. Like a Person, an Entity might have a number of
22
24
  # roles, such as author, contact, editor, etc.
@@ -28,11 +30,12 @@ module CFF
28
30
  # parentheses):
29
31
  #
30
32
  # * `address`
33
+ # * `alias`
31
34
  # * `city`
32
35
  # * `country`
33
- # * `email`
34
36
  # * `date_end` - *Note:* returns a `Date` object
35
37
  # * `date_start` - *Note:* returns a `Date` object
38
+ # * `email`
36
39
  # * `fax`
37
40
  # * `location`
38
41
  # * `name`
@@ -42,11 +45,9 @@ module CFF
42
45
  # * `tel`
43
46
  # * `website`
44
47
  class Entity < ModelPart
48
+ ALLOWED_FIELDS = SCHEMA_FILE['definitions']['entity']['properties'].keys.freeze # :nodoc:
45
49
 
46
- ALLOWED_FIELDS = [
47
- 'address', 'city', 'country', 'email', 'date-end', 'date-start', 'fax',
48
- 'location', 'name', 'orcid', 'post-code', 'region', 'tel', 'website'
49
- ].freeze # :nodoc:
50
+ attr_date :date_end, :date_start
50
51
 
51
52
  # :call-seq:
52
53
  # new(name) -> Entity
@@ -54,37 +55,16 @@ module CFF
54
55
  #
55
56
  # Create a new Entity with the supplied name.
56
57
  def initialize(param)
58
+ super()
59
+
57
60
  if param.is_a?(Hash)
58
61
  @fields = param
59
- @fields.default = ''
60
62
  else
61
- @fields = Hash.new('')
63
+ @fields = {}
62
64
  @fields['name'] = param
63
65
  end
64
66
 
65
67
  yield self if block_given?
66
68
  end
67
-
68
- # :call-seq:
69
- # date_end = date
70
- #
71
- # Set the `date-end` field. If a non-Date object is passed in it will
72
- # be parsed into a Date.
73
- def date_end=(date)
74
- date = Date.parse(date) unless date.is_a?(Date)
75
-
76
- @fields['date-end'] = date
77
- end
78
-
79
- # :call-seq:
80
- # date_start = date
81
- #
82
- # Set the `date-start` field. If a non-Date object is passed in it will
83
- # be parsed into a Date.
84
- def date_start=(date)
85
- date = Date.parse(date) unless date.is_a?(Date)
86
-
87
- @fields['date-start'] = date
88
- end
89
69
  end
90
70
  end
data/lib/cff/errors.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright (c) 2018-2021 The Ruby Citation File Format Developers.
3
+ # Copyright (c) 2018-2022 The Ruby Citation File Format Developers.
4
4
  #
5
5
  # Licensed under the Apache License, Version 2.0 (the "License");
6
6
  # you may not use this file except in compliance with the License.
@@ -16,30 +16,35 @@
16
16
 
17
17
  ##
18
18
  module CFF
19
-
20
19
  # Error is the base class for all errors raised by this library.
21
20
  class Error < RuntimeError
22
-
23
21
  def initialize(message = nil) # :nodoc:
24
22
  super
25
23
  end
26
24
  end
27
25
 
28
- # ValidationError is raised when a CFF file fails validatedation. It
29
- # contains details of each failure that was detected by the underlying
30
- # JsonSchema library, which is used to perform the validation.
26
+ # ValidationError is raised when a CFF file fails validation. It contains
27
+ # details of each failure that was detected by the underlying JsonSchema
28
+ # library, which is used to perform the validation.
29
+ #
30
+ # Additionally, the `invalid_filename` flag is used to indicate whether the
31
+ # CFF file is named correctly. This is only used when validating a File;
32
+ # validating a Index directly will not set this flag to `true`.
31
33
  class ValidationError < Error
32
-
33
34
  # The list of JsonSchema::ValidationErrors found by the validator.
34
35
  attr_reader :errors
35
36
 
36
- def initialize(errors) # :nodoc:
37
+ # If a File was validated, was its filename invalid?
38
+ attr_reader :invalid_filename
39
+
40
+ def initialize(errors, invalid_filename: false) # :nodoc:
37
41
  super('Validation error')
38
42
  @errors = errors
43
+ @invalid_filename = invalid_filename
39
44
  end
40
45
 
41
46
  def to_s # :nodoc:
42
- "#{super}: #{@errors.join(' ')}"
47
+ "#{super}: (Invalid filename: #{@invalid_filename}) #{@errors.join(' ')}"
43
48
  end
44
49
  end
45
50
  end
data/lib/cff/file.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright (c) 2018-2021 The Ruby Citation File Format Developers.
3
+ # Copyright (c) 2018-2022 The Ruby Citation File Format Developers.
4
4
  #
5
5
  # Licensed under the Apache License, Version 2.0 (the "License");
6
6
  # you may not use this file except in compliance with the License.
@@ -14,13 +14,23 @@
14
14
  # See the License for the specific language governing permissions and
15
15
  # limitations under the License.
16
16
 
17
+ require_relative 'errors'
18
+ require_relative 'index'
19
+ require_relative 'version'
20
+
21
+ require 'date'
22
+ require 'yaml'
23
+
17
24
  ##
18
25
  module CFF
19
-
20
- # File provides direct access to a CFF Model, with the addition of some
26
+ # File provides direct access to a CFF Index, with the addition of some
21
27
  # filesystem utilities.
28
+ #
29
+ # To be a fully compliant and valid CFF file its filename should be
30
+ # 'CITATION.cff'. This class allows you to create files with any filename,
31
+ # and to validate the contents of those files independently of the preferred
32
+ # filename.
22
33
  class File
23
-
24
34
  # A comment to be inserted at the top of the resultant CFF file.
25
35
  attr_reader :comment
26
36
 
@@ -33,27 +43,28 @@ module CFF
33
43
  'Gem: https://rubygems.org/gems/cff',
34
44
  'CFF: https://citation-file-format.github.io/'
35
45
  ].freeze # :nodoc:
46
+ CFF_VALID_FILENAME = 'CITATION.cff' # :nodoc:
36
47
 
37
48
  # :call-seq:
38
49
  # new(filename, title) -> File
39
- # new(filename, model) -> File
50
+ # new(filename, index) -> File
40
51
  #
41
- # Create a new File. Either a pre-existing Model can be passed in or, as
42
- # with Model itself, a title can be supplied to initalize a new File.
52
+ # Create a new File. Either a pre-existing Index can be passed in or, as
53
+ # with Index itself, a title can be supplied to initalize a new File.
43
54
  #
44
- # All methods provided by Model are also available directly on File
55
+ # All methods provided by Index are also available directly on File
45
56
  # objects.
46
57
  def initialize(filename, param, comment = CFF_COMMENT, create: false)
47
- param = Model.new(param) unless param.is_a?(Model)
58
+ param = Index.new(param) unless param.is_a?(Index)
48
59
 
49
60
  @filename = filename
50
- @model = param
61
+ @index = param
51
62
  @comment = comment
52
63
  @dirty = create
53
64
  end
54
65
 
55
66
  # :call-seq:
56
- # read(file) -> File
67
+ # read(filename) -> File
57
68
  #
58
69
  # Read a file and parse it for subsequent manipulation.
59
70
  def self.read(file)
@@ -66,8 +77,8 @@ module CFF
66
77
  end
67
78
 
68
79
  # :call-seq:
69
- # open(file) -> File
70
- # open(file) {|cff| block }
80
+ # open(filename) -> File
81
+ # open(filename) { |cff| block }
71
82
  #
72
83
  # With no associated block, File.open is a synonym for ::read. If the
73
84
  # optional code block is given, it will be passed the opened file as an
@@ -97,31 +108,41 @@ module CFF
97
108
  end
98
109
 
99
110
  # :call-seq:
100
- # validate(file) -> Array
111
+ # validate(filename, fail_on_filename: true) -> Array
101
112
  #
102
113
  # Read a file and return an array with the result. The result array is a
103
- # two-element array, with `true`/`false` at index 0 to indicate pass/fail,
104
- # and an array of errors at index 1 (if any).
105
- def self.validate(file)
106
- File.read(file).validate
114
+ # three-element array, with `true`/`false` at index 0 to indicate
115
+ # pass/fail, an array of schema validation errors at index 1 (if any), and
116
+ # `true`/`false` at index 2 to indicate whether the filename passed/failed
117
+ # validation.
118
+ #
119
+ # You can choose whether filename validation failure should cause overall
120
+ # validation failure with the `fail_on_filename` parameter (default: true).
121
+ def self.validate(file, fail_on_filename: true)
122
+ File.read(file).validate(fail_on_filename: fail_on_filename)
107
123
  end
108
124
 
109
125
  # :call-seq:
110
- # validate!(file)
126
+ # validate!(filename, fail_on_filename: true)
111
127
  #
112
128
  # Read a file and raise a ValidationError upon failure. If an error is
113
129
  # raised it will contain the detected validation failures for further
114
130
  # inspection.
115
- def self.validate!(file)
116
- File.read(file).validate!
131
+ #
132
+ # You can choose whether filename validation failure should cause overall
133
+ # validation failure with the `fail_on_filename` parameter (default: true).
134
+ def self.validate!(file, fail_on_filename: true)
135
+ File.read(file).validate!(fail_on_filename: fail_on_filename)
117
136
  end
118
137
 
119
138
  # :call-seq:
120
- # write(file, model)
121
- # write(file, yaml)
139
+ # write(filename, File)
140
+ # write(filename, Index)
141
+ # write(filename, yaml)
122
142
  #
123
- # Write the supplied model or yaml string to `file`.
143
+ # Write the supplied File, Index or yaml string to `file`.
124
144
  def self.write(file, cff, comment = '')
145
+ comment = cff.comment if cff.respond_to?(:comment)
125
146
  cff = cff.to_yaml unless cff.is_a?(String)
126
147
  content = File.format_comment(comment) + cff[YAML_HEADER.length...-1]
127
148
 
@@ -129,11 +150,56 @@ module CFF
129
150
  end
130
151
 
131
152
  # :call-seq:
132
- # write
153
+ # validate(fail_fast: false, fail_on_filename: true) -> Array
154
+ #
155
+ # Validate this file and return an array with the result. The result array
156
+ # is a three-element array, with `true`/`false` at index 0 to indicate
157
+ # pass/fail, an array of schema validation errors at index 1 (if any), and
158
+ # `true`/`false` at index 2 to indicate whether the filename passed/failed
159
+ # validation.
160
+ #
161
+ # You can choose whether filename validation failure should cause overall
162
+ # validation failure with the `fail_on_filename` parameter (default: true).
163
+ def validate(fail_fast: false, fail_on_filename: true)
164
+ valid_filename = (::File.basename(@filename) == CFF_VALID_FILENAME)
165
+ result = (@index.validate(fail_fast: fail_fast) << valid_filename)
166
+ result[0] &&= valid_filename if fail_on_filename
167
+
168
+ result
169
+ end
170
+
171
+ # :call-seq:
172
+ # validate!(fail_fast: false, fail_on_filename: true)
133
173
  #
134
- # Write this CFF File.
135
- def write
136
- File.write(@filename, @model, @comment) if @dirty
174
+ # Validate this file and raise a ValidationError upon failure. If an error
175
+ # is raised it will contain the detected validation failures for further
176
+ # inspection.
177
+ #
178
+ # You can choose whether filename validation failure should cause overall
179
+ # validation failure with the `fail_on_filename` parameter (default: true).
180
+ def validate!(fail_fast: false, fail_on_filename: true)
181
+ result = validate(
182
+ fail_fast: fail_fast, fail_on_filename: fail_on_filename
183
+ )
184
+ return if result[0]
185
+
186
+ raise ValidationError.new(result[1], invalid_filename: !result[2])
187
+ end
188
+
189
+ # :call-seq:
190
+ # write(save_as: filename)
191
+ #
192
+ # Write this CFF File. The `save_as` parameter can be used to save a new
193
+ # copy of this CFF File under a different filename, leaving the original
194
+ # file untouched. If `save_as` is used then the internal filename of the
195
+ # File will be updated to the supplied filename.
196
+ def write(save_as: nil)
197
+ unless save_as.nil?
198
+ @filename = save_as
199
+ @dirty = true
200
+ end
201
+
202
+ File.write(@filename, @index, @comment) if @dirty
137
203
  @dirty = false
138
204
  end
139
205
 
@@ -156,17 +222,21 @@ module CFF
156
222
  @comment = comment
157
223
  end
158
224
 
225
+ def to_yaml # :nodoc:
226
+ @index.to_yaml
227
+ end
228
+
159
229
  def method_missing(name, *args) # :nodoc:
160
- if @model.respond_to?(name)
230
+ if @index.respond_to?(name)
161
231
  @dirty = true if name.to_s.end_with?('=') # Remove to_s when Ruby >2.6.
162
- @model.send(name, *args)
232
+ @index.send(name, *args)
163
233
  else
164
234
  super
165
235
  end
166
236
  end
167
237
 
168
238
  def respond_to_missing?(name, *all) # :nodoc:
169
- @model.respond_to?(name, *all)
239
+ @index.respond_to?(name, *all)
170
240
  end
171
241
 
172
242
  def self.format_comment(comment) # :nodoc:
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2018-2022 The Ruby Citation File Format Developers.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require_relative 'apalike'
18
+ require_relative 'bibtex'
19
+
20
+ ##
21
+ module CFF
22
+ module Formatters # :nodoc:
23
+ register_formatter(APALike)
24
+ register_formatter(BibTeX)
25
+ end
26
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2018-2022 The Ruby Citation File Format Developers.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require_relative 'formatter'
18
+
19
+ ##
20
+ module CFF
21
+ module Formatters # :nodoc:
22
+ # Generates an APALIKE citation string
23
+ class APALike < Formatter # :nodoc:
24
+ def self.format(model:, preferred_citation: true) # rubocop:disable Metrics/AbcSize
25
+ model = select_and_check_model(model, preferred_citation)
26
+ return if model.nil?
27
+
28
+ output = []
29
+ output << combine_authors(
30
+ model.authors.map { |author| format_author(author) }
31
+ )
32
+
33
+ date = month_and_year_from_model(model)
34
+ output << "(#{date})" unless date.empty?
35
+
36
+ version = " (Version #{model.version})" unless model.version.to_s.empty?
37
+ output << "#{model.title}#{version}#{type_label(model)}"
38
+ output << publication_data_from_model(model)
39
+ output << url(model)
40
+
41
+ output.reject(&:empty?).join('. ')
42
+ end
43
+
44
+ def self.publication_data_from_model(model) # rubocop:disable Metrics
45
+ case model.type
46
+ when 'article'
47
+ [
48
+ model.journal,
49
+ volume_from_model(model),
50
+ pages_from_model(model, dash: '–'),
51
+ note_from_model(model) || ''
52
+ ].reject(&:empty?).join(', ')
53
+ when 'book'
54
+ model.publisher.empty? ? '' : model.publisher.name
55
+ when 'conference-paper'
56
+ [
57
+ model.collection_title,
58
+ volume_from_model(model),
59
+ pages_from_model(model, dash: '–')
60
+ ].reject(&:empty?).join(', ')
61
+ when 'report'
62
+ if model.institution.empty?
63
+ model.authors.first.affiliation
64
+ else
65
+ model.institution.name
66
+ end
67
+ when 'phdthesis'
68
+ type_and_school_from_model(model, 'Doctoral dissertation')
69
+ when 'mastersthesis'
70
+ type_and_school_from_model(model, "Master's thesis")
71
+ when 'unpublished'
72
+ note_from_model(model) || ''
73
+ else
74
+ ''
75
+ end
76
+ end
77
+
78
+ def self.type_and_school_from_model(model, type)
79
+ type = model.thesis_type == '' ? type : model.thesis_type
80
+ school = model.institution.empty? ? model.authors.first.affiliation : model.institution.name
81
+ "[#{type}, #{school}]"
82
+ end
83
+
84
+ def self.volume_from_model(model)
85
+ issue = model.issue.to_s.empty? ? '' : "(#{model.issue})"
86
+ model.volume.to_s.empty? ? '' : "#{model.volume}#{issue}"
87
+ end
88
+
89
+ # If we're citing a conference paper, try and use the date of the
90
+ # conference. Otherwise use the specified month and year, or the date
91
+ # of release.
92
+ def self.month_and_year_from_model(model)
93
+ if model.type == 'conference-paper' && !model.conference.empty?
94
+ start = model.conference.date_start
95
+ unless start == ''
96
+ finish = model.conference.date_end
97
+ return month_and_year_from_date(start)[1] if finish == '' || start >= finish
98
+
99
+ return date_range(start, finish)
100
+ end
101
+ end
102
+
103
+ super[1]
104
+ end
105
+
106
+ def self.date_range(start, finish)
107
+ start_str = '%Y, %B %-d'
108
+ finish_str = '%-d'
109
+ finish_str = "%B #{finish_str}" unless start.month == finish.month
110
+ finish_str = "%Y, #{finish_str}" unless start.year == finish.year
111
+
112
+ "#{start.strftime(start_str)}–#{finish.strftime(finish_str)}"
113
+ end
114
+
115
+ # Prefer a DOI over the other URI options.
116
+ def self.url(model)
117
+ model.doi.empty? ? super : "https://doi.org/#{model.doi}"
118
+ end
119
+
120
+ def self.type_label(model)
121
+ return ' [Data set]' if model.type.include?('data')
122
+ return ' [Conference paper]' if model.type.include?('conference')
123
+ return '' if model.is_a?(Reference) && !model.type.include?('software')
124
+
125
+ ' [Computer software]'
126
+ end
127
+
128
+ def self.combine_authors(authors)
129
+ return authors[0].chomp('.') if authors.length == 1
130
+
131
+ "#{authors[0..-2].join(', ')}, & #{authors[-1]}".chomp('.')
132
+ end
133
+
134
+ def self.format_author(author)
135
+ return author.name if author.is_a?(Entity)
136
+
137
+ particle =
138
+ author.name_particle.empty? ? '' : "#{author.name_particle} "
139
+ suffix = author.name_suffix.empty? ? '.' : "., #{author.name_suffix}"
140
+
141
+ "#{particle}#{author.family_names}, #{initials(author.given_names)}#{suffix}"
142
+ end
143
+ end
144
+ end
145
+ end