cff 0.1.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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