cff 0.8.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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