cff 0.1.0 → 0.8.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/Rakefile CHANGED
@@ -1,4 +1,6 @@
1
- # Copyright (c) 2018 Robert Haines.
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2018-2021 The Ruby Citation File Format Developers.
2
4
  #
3
5
  # Licensed under the Apache License, Version 2.0 (the "License");
4
6
  # you may not use this file except in compliance with the License.
@@ -12,22 +14,25 @@
12
14
  # See the License for the specific language governing permissions and
13
15
  # limitations under the License.
14
16
 
15
- require "bundler/gem_tasks"
16
- require "rake/testtask"
17
- require "rdoc/task"
17
+ require 'bundler/gem_tasks'
18
+ require 'rake/testtask'
19
+ require 'rdoc/task'
20
+ require 'rubocop/rake_task'
18
21
 
19
- task :default => :test
22
+ task default: :test
20
23
 
21
24
  Rake::TestTask.new(:test) do |t|
22
- t.libs << "test"
23
- t.libs << "lib"
24
- t.test_files = FileList["test/**/*_test.rb"]
25
+ t.libs << 'test'
26
+ t.libs << 'lib'
27
+ t.test_files = FileList['test/**/*_test.rb']
25
28
  end
26
29
 
27
30
  RDoc::Task.new do |r|
28
- r.main = "README.md"
29
- r.rdoc_files.include("README.md", "LICENCE", "lib/**/*.rb")
30
- r.options << "--markup=markdown"
31
- r.options << "--tab-width=2"
31
+ r.main = 'README.md'
32
+ r.rdoc_files.include('README.md', 'LICENCE', 'CHANGES.md', 'lib/**/*.rb')
33
+ r.options << '--markup=markdown'
34
+ r.options << '--tab-width=2'
32
35
  r.options << "-t Ruby CFF Library version #{::CFF::VERSION}"
33
36
  end
37
+
38
+ RuboCop::RakeTask.new
data/bin/console CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
- require "bundler/setup"
4
- require "cff"
4
+ require 'bundler/setup'
5
+ require 'cff'
5
6
 
6
7
  # You can add fixtures and/or initialization code here to make experimenting
7
8
  # with your gem easier. You can also use a different console, if you like.
@@ -10,5 +11,5 @@ require "cff"
10
11
  # require "pry"
11
12
  # Pry.start
12
13
 
13
- require "irb"
14
+ require 'irb'
14
15
  IRB.start(__FILE__)
data/cff.gemspec CHANGED
@@ -1,4 +1,6 @@
1
- # Copyright (c) 2018 Robert Haines.
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2018-2021 The Ruby Citation File Format Developers.
2
4
  #
3
5
  # Licensed under the Apache License, Version 2.0 (the "License");
4
6
  # you may not use this file except in compliance with the License.
@@ -12,31 +14,51 @@
12
14
  # See the License for the specific language governing permissions and
13
15
  # limitations under the License.
14
16
 
15
- lib = File.expand_path("../lib", __FILE__)
16
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
17
- require "cff/version"
17
+ require_relative 'lib/cff/version'
18
18
 
19
19
  Gem::Specification.new do |spec|
20
- spec.name = "cff"
20
+ spec.name = 'cff'
21
21
  spec.version = CFF::VERSION
22
- spec.authors = ["Robert Haines"]
23
- spec.email = ["robert.haines@manchester.ac.uk"]
22
+ spec.authors = [
23
+ 'Robert Haines',
24
+ 'The Ruby Citation File Format Developers'
25
+ ]
26
+ spec.email = ['robert.haines@manchester.ac.uk']
27
+
28
+ spec.summary = 'A Ruby library for manipulating CITATION.cff files.'
29
+ spec.description = 'See https://citation-file-format.github.io/ ' \
30
+ 'for more info.'
31
+ spec.homepage = 'https://github.com/citation-file-format/ruby-cff'
32
+ spec.license = 'Apache-2.0'
24
33
 
25
- spec.summary = "A Ruby library for manipulating CITATION.cff files."
26
- spec.description = "See https://citation-file-format.github.io/ for more info."
27
- spec.homepage = "https://github.com/hainesr/ruby-cff"
34
+ spec.metadata = {
35
+ 'bug_tracker_uri' => 'https://github.com/citation-file-format/ruby-cff/issues',
36
+ 'changelog_uri' => 'https://github.com/citation-file-format/ruby-cff/blob/main/CHANGES.md',
37
+ 'documentation_uri' => 'https://citation-file-format.github.io/ruby-cff/',
38
+ 'source_code_uri' => 'https://github.com/citation-file-format/ruby-cff'
39
+ }
28
40
 
29
- spec.files = `git ls-files -z`.split("\x0").reject do |f|
30
- f.match(%r{^(test|spec|features)/})
41
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
42
+ f.match(%r{^((test|spec|features)/|\.)})
31
43
  end
32
- spec.bindir = "exe"
44
+
45
+ spec.bindir = 'exe'
33
46
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
34
- spec.require_paths = ["lib"]
35
-
36
- spec.add_development_dependency "bundler", "~> 1.16"
37
- spec.add_development_dependency "rake", "~> 10.0"
38
- spec.add_development_dependency "minitest", "~> 5.0"
39
- spec.add_development_dependency "coveralls", "~> 0.8"
40
- spec.add_development_dependency "test_construct", "~> 2.0"
41
- spec.add_development_dependency "rdoc", "~> 6.0"
47
+ spec.require_paths = ['lib']
48
+
49
+ spec.required_ruby_version = '>= 2.6'
50
+
51
+ spec.add_runtime_dependency 'json_schema', '~> 0.21.0'
52
+ spec.add_runtime_dependency 'language_list', '~> 1.2'
53
+
54
+ spec.add_development_dependency 'minitest', '~> 5.14'
55
+ spec.add_development_dependency 'rake', '~> 13.0'
56
+ spec.add_development_dependency 'rdoc', '~> 6.3'
57
+ spec.add_development_dependency 'rubocop', '~> 1.15'
58
+ spec.add_development_dependency 'rubocop-minitest', '~> 0.13'
59
+ spec.add_development_dependency 'rubocop-performance', '~> 1.11.0'
60
+ spec.add_development_dependency 'rubocop-rake', '~> 0.5.0'
61
+ spec.add_development_dependency 'simplecov', '~> 0.20.0'
62
+ spec.add_development_dependency 'simplecov-lcov', '~> 0.8.0'
63
+ spec.add_development_dependency 'test_construct', '~> 2.0'
42
64
  end
data/lib/cff.rb CHANGED
@@ -1,4 +1,6 @@
1
- # Copyright (c) 2018 Robert Haines.
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2018-2021 The Ruby Citation File Format Developers.
2
4
  #
3
5
  # Licensed under the Apache License, Version 2.0 (the "License");
4
6
  # you may not use this file except in compliance with the License.
@@ -12,14 +14,11 @@
12
14
  # See the License for the specific language governing permissions and
13
15
  # limitations under the License.
14
16
 
15
- require "date"
16
- require "yaml"
17
+ require 'date'
18
+ require 'json'
19
+ require 'yaml'
17
20
 
18
- require "cff/version"
19
- require "cff/person"
20
- require "cff/entity"
21
- require "cff/model"
22
- require "cff/file"
21
+ require 'language_list'
23
22
 
24
23
  # This library provides a Ruby interface to manipulate CITATION.cff files. The
25
24
  # primary entry points are Model and File.
@@ -27,5 +26,22 @@ require "cff/file"
27
26
  # See the [CITATION.cff documentation](https://citation-file-format.github.io/)
28
27
  # for more details.
29
28
  module CFF
30
-
29
+ SCHEMA_PATH = ::File.join(__dir__, 'schema', '1.2.0.json') # :nodoc:
30
+ SCHEMA_FILE = JSON.parse(::File.read(SCHEMA_PATH)) # :nodoc:
31
31
  end
32
+
33
+ require 'cff/version'
34
+ require 'cff/errors'
35
+ require 'cff/util'
36
+ require 'cff/licensable'
37
+ require 'cff/validatable'
38
+ require 'cff/model_part'
39
+ require 'cff/person'
40
+ require 'cff/entity'
41
+ require 'cff/identifier'
42
+ require 'cff/reference'
43
+ require 'cff/model'
44
+ require 'cff/file'
45
+ require 'cff/formatter/formatter'
46
+ require 'cff/formatter/apa_formatter'
47
+ require 'cff/formatter/bibtex_formatter'
data/lib/cff/entity.rb CHANGED
@@ -1,4 +1,6 @@
1
- # Copyright (c) 2018 Robert Haines.
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2018-2021 The Ruby Citation File Format Developers.
2
4
  #
3
5
  # Licensed under the Apache License, Version 2.0 (the "License");
4
6
  # you may not use this file except in compliance with the License.
@@ -12,24 +14,77 @@
12
14
  # See the License for the specific language governing permissions and
13
15
  # limitations under the License.
14
16
 
15
- #
17
+ ##
16
18
  module CFF
17
19
 
18
20
  # An Entity can represent different types of entities, e.g., a publishing
19
21
  # company, or conference. Like a Person, an Entity might have a number of
20
22
  # roles, such as author, contact, editor, etc.
21
- class Entity
23
+ #
24
+ # Entity implements all of the fields listed in the
25
+ # [CFF standard](https://citation-file-format.github.io/). All fields
26
+ # are simple strings and can be set as such. A field which has not been set
27
+ # will return the empty string. The simple fields are (with defaults in
28
+ # parentheses):
29
+ #
30
+ # * `address`
31
+ # * `city`
32
+ # * `country`
33
+ # * `email`
34
+ # * `date_end` - *Note:* returns a `Date` object
35
+ # * `date_start` - *Note:* returns a `Date` object
36
+ # * `fax`
37
+ # * `location`
38
+ # * `name`
39
+ # * `orcid`
40
+ # * `post_code`
41
+ # * `region`
42
+ # * `tel`
43
+ # * `website`
44
+ class Entity < ModelPart
22
45
 
23
- attr_reader :fields # :nodoc:
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:
24
50
 
25
51
  # :call-seq:
26
52
  # new(name) -> Entity
53
+ # new(name) { |entity| block } -> Entity
27
54
  #
28
55
  # Create a new Entity with the supplied name.
29
- def initialize(name)
30
- @fields = Hash.new('')
31
- @fields['name'] = name
56
+ def initialize(param)
57
+ if param.is_a?(Hash)
58
+ @fields = param
59
+ @fields.default = ''
60
+ else
61
+ @fields = Hash.new('')
62
+ @fields['name'] = param
63
+ end
64
+
65
+ yield self if block_given?
66
+ 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
32
77
  end
33
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
34
89
  end
35
90
  end
data/lib/cff/errors.rb ADDED
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2018-2021 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
+ ##
18
+ module CFF
19
+
20
+ # Error is the base class for all errors raised by this library.
21
+ class Error < RuntimeError
22
+
23
+ def initialize(message = nil) # :nodoc:
24
+ super
25
+ end
26
+ end
27
+
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.
31
+ class ValidationError < Error
32
+
33
+ # The list of JsonSchema::ValidationErrors found by the validator.
34
+ attr_reader :errors
35
+
36
+ def initialize(errors) # :nodoc:
37
+ super('Validation error')
38
+ @errors = errors
39
+ end
40
+
41
+ def to_s # :nodoc:
42
+ "#{super}: #{@errors.join(' ')}"
43
+ end
44
+ end
45
+ end
data/lib/cff/file.rb CHANGED
@@ -1,4 +1,6 @@
1
- # Copyright (c) 2018 Robert Haines.
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2018-2021 The Ruby Citation File Format Developers.
2
4
  #
3
5
  # Licensed under the Apache License, Version 2.0 (the "License");
4
6
  # you may not use this file except in compliance with the License.
@@ -12,30 +14,42 @@
12
14
  # See the License for the specific language governing permissions and
13
15
  # limitations under the License.
14
16
 
15
- #
17
+ ##
16
18
  module CFF
17
19
 
18
20
  # File provides direct access to a CFF Model, with the addition of some
19
21
  # filesystem utilities.
20
22
  class File
21
23
 
24
+ # A comment to be inserted at the top of the resultant CFF file.
25
+ attr_reader :comment
26
+
27
+ # The filename of this CFF file.
28
+ attr_reader :filename
29
+
22
30
  YAML_HEADER = "---\n" # :nodoc:
31
+ CFF_COMMENT = [
32
+ "This CITATION.cff file was created by ruby-cff (v #{CFF::VERSION}).",
33
+ 'Gem: https://rubygems.org/gems/cff',
34
+ 'CFF: https://citation-file-format.github.io/'
35
+ ].freeze # :nodoc:
23
36
 
24
37
  # :call-seq:
25
- # new(title) -> File
26
- # new(model) -> File
38
+ # new(filename, title) -> File
39
+ # new(filename, model) -> File
27
40
  #
28
41
  # Create a new File. Either a pre-existing Model can be passed in or, as
29
42
  # with Model itself, a title can be supplied to initalize a new File.
30
43
  #
31
44
  # All methods provided by Model are also available directly on File
32
45
  # objects.
33
- def initialize(param)
34
- unless Model === param
35
- param = Model.new(param)
36
- end
46
+ def initialize(filename, param, comment = CFF_COMMENT, create: false)
47
+ param = Model.new(param) unless param.is_a?(Model)
37
48
 
49
+ @filename = filename
38
50
  @model = param
51
+ @comment = comment
52
+ @dirty = create
39
53
  end
40
54
 
41
55
  # :call-seq:
@@ -43,7 +57,63 @@ module CFF
43
57
  #
44
58
  # Read a file and parse it for subsequent manipulation.
45
59
  def self.read(file)
46
- new(YAML::load_file(file))
60
+ content = ::File.read(file)
61
+ comment = File.parse_comment(content)
62
+
63
+ new(
64
+ file, YAML.safe_load(content, permitted_classes: [Date, Time]), comment
65
+ )
66
+ end
67
+
68
+ # :call-seq:
69
+ # open(file) -> File
70
+ # open(file) {|cff| block }
71
+ #
72
+ # With no associated block, File.open is a synonym for ::read. If the
73
+ # optional code block is given, it will be passed the opened file as an
74
+ # argument and the File object will automatically be written (if edited)
75
+ # and closed when the block terminates.
76
+ #
77
+ # File.open will create a new file if one does not already exist with the
78
+ # provided file name.
79
+ def self.open(file)
80
+ if ::File.exist?(file)
81
+ content = ::File.read(file)
82
+ comment = File.parse_comment(content)
83
+ yaml = YAML.safe_load(content, permitted_classes: [Date, Time])
84
+ else
85
+ comment = CFF_COMMENT
86
+ yaml = ''
87
+ end
88
+
89
+ cff = new(file, yaml, comment, create: (yaml == ''))
90
+ return cff unless block_given?
91
+
92
+ begin
93
+ yield cff
94
+ ensure
95
+ cff.write
96
+ end
97
+ end
98
+
99
+ # :call-seq:
100
+ # validate(file) -> Array
101
+ #
102
+ # 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
107
+ end
108
+
109
+ # :call-seq:
110
+ # validate!(file)
111
+ #
112
+ # Read a file and raise a ValidationError upon failure. If an error is
113
+ # raised it will contain the detected validation failures for further
114
+ # inspection.
115
+ def self.validate!(file)
116
+ File.read(file).validate!
47
117
  end
48
118
 
49
119
  # :call-seq:
@@ -51,19 +121,73 @@ module CFF
51
121
  # write(file, yaml)
52
122
  #
53
123
  # Write the supplied model or yaml string to `file`.
54
- def self.write(file, cff)
55
- unless String === cff
56
- cff = cff.to_yaml
57
- end
124
+ def self.write(file, cff, comment = '')
125
+ cff = cff.to_yaml unless cff.is_a?(String)
126
+ content = File.format_comment(comment) + cff[YAML_HEADER.length...-1]
127
+
128
+ ::File.write(file, content)
129
+ end
58
130
 
59
- ::File.write(file, cff[YAML_HEADER.length...-1])
131
+ # :call-seq:
132
+ # write
133
+ #
134
+ # Write this CFF File.
135
+ def write
136
+ File.write(@filename, @model, @comment) if @dirty
137
+ @dirty = false
138
+ end
139
+
140
+ # :call-seq:
141
+ # comment = string or array
142
+ #
143
+ # A comment to be inserted at the top of the resultant CFF file. This can
144
+ # be supplied as a simple string or an array of strings. When the file is
145
+ # saved this comment is formatted as follows:
146
+ #
147
+ # * a simple string is split into 75 character lines and `'# '` is prepended
148
+ # to each line;
149
+ # * an array of strings is joined into a single string with `'\n'` and
150
+ # `'# '` is prepended to each line;
151
+ #
152
+ # If you care about formatting, use an array of strings for your comment,
153
+ # if not, use a single string.
154
+ def comment=(comment)
155
+ @dirty = true
156
+ @comment = comment
60
157
  end
61
158
 
62
159
  def method_missing(name, *args) # :nodoc:
63
- super unless Model::ALLOWED_METHODS.include?(name)
160
+ if @model.respond_to?(name)
161
+ @dirty = true if name.to_s.end_with?('=') # Remove to_s when Ruby >2.6.
162
+ @model.send(name, *args)
163
+ else
164
+ super
165
+ end
166
+ end
64
167
 
65
- @model.send name, args
168
+ def respond_to_missing?(name, *all) # :nodoc:
169
+ @model.respond_to?(name, *all)
66
170
  end
67
171
 
172
+ def self.format_comment(comment) # :nodoc:
173
+ return '' if comment.empty?
174
+
175
+ comment = comment.scan(/.{1,75}/) if comment.is_a?(String)
176
+ c = comment.map do |l|
177
+ l.empty? ? '#' : "# #{l}"
178
+ end.join("\n")
179
+
180
+ "#{c}\n\n"
181
+ end
182
+
183
+ def self.parse_comment(content) # :nodoc:
184
+ content = content.split("\n")
185
+
186
+ content.reduce([]) do |acc, line|
187
+ break acc unless line.start_with?('#')
188
+
189
+ acc << line.sub(/^#+/, '').strip
190
+ end
191
+ end
68
192
  end
69
193
  end