Class: Philiprehberger::ChangelogParser::Changelog

Inherits:
Object
  • Object
show all
Defined in:
lib/philiprehberger/changelog_parser/changelog.rb

Overview

Represents a parsed changelog with version entries

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(title:, preamble:, entries:) ⇒ Changelog

Returns a new instance of Changelog.

Parameters:

  • title (String)

    the changelog title

  • preamble (String)

    text before first version

  • entries (Array<VersionEntry>)

    parsed version entries



10
11
12
13
14
# File 'lib/philiprehberger/changelog_parser/changelog.rb', line 10

def initialize(title:, preamble:, entries:)
  @title = title
  @preamble = preamble
  @entries = entries
end

Instance Attribute Details

#preambleString (readonly)

Returns the preamble text.

Returns:

  • (String)

    the preamble text



20
21
22
# File 'lib/philiprehberger/changelog_parser/changelog.rb', line 20

def preamble
  @preamble
end

#titleString (readonly)

Returns the changelog title.

Returns:

  • (String)

    the changelog title



17
18
19
# File 'lib/philiprehberger/changelog_parser/changelog.rb', line 17

def title
  @title
end

Class Method Details

.from_json(json_string) ⇒ Changelog

Deserialize a changelog from a JSON string

Parameters:

  • json_string (String)

    JSON produced by #to_json

Returns:



220
221
222
223
224
225
226
227
228
229
230
# File 'lib/philiprehberger/changelog_parser/changelog.rb', line 220

def self.from_json(json_string)
  require 'json'
  data = JSON.parse(json_string)

  entries = data['versions'].map do |v|
    categories = v['categories'].transform_values(&:dup)
    VersionEntry.new(version: v['version'], date: v['date'], categories: categories)
  end

  new(title: data['title'], preamble: '', entries: entries)
end

Instance Method Details

#add(version_string, category, entry) ⇒ void

This method returns an undefined value.

Add an entry to a version under a category

Parameters:

  • version_string (String)

    the version to add to

  • category (String)

    the category (Added, Changed, Fixed, etc.)

  • entry (String)

    the entry text

Raises:



71
72
73
74
75
76
# File 'lib/philiprehberger/changelog_parser/changelog.rb', line 71

def add(version_string, category, entry)
  ver = version(version_string)
  raise Error, "version #{version_string} not found" unless ver

  ver.add_entry(category, entry)
end

#categoriesArray<String>

Return sorted unique category names present across all version entries

Returns:

  • (Array<String>)

    sorted unique category names



40
41
42
# File 'lib/philiprehberger/changelog_parser/changelog.rb', line 40

def categories
  @entries.flat_map { |e| e.categories.keys }.uniq.sort
end

#diff(from_version, to_version) ⇒ Hash<String, Array<String>>

Returns combined entries for all versions between from_version (exclusive) and to_version (inclusive).

Parameters:

  • from_version (String)

    starting version (exclusive)

  • to_version (String)

    ending version (inclusive)

Returns:

  • (Hash<String, Array<String>>)

    merged categories with entries

Raises:

  • (Error)

    if either version is not found



105
106
107
108
109
110
111
112
113
114
# File 'lib/philiprehberger/changelog_parser/changelog.rb', line 105

def diff(from_version, to_version)
  from_idx = @entries.index { |e| e.version == from_version }
  to_idx = @entries.index { |e| e.version == to_version }
  raise Philiprehberger::ChangelogParser::Error, "version not found: #{from_version}" unless from_idx
  raise Philiprehberger::ChangelogParser::Error, "version not found: #{to_version}" unless to_idx

  low, high = [from_idx, to_idx].sort
  range = @entries[low..high].reject { |e| e.version == from_version }
  merge_categories(range)
end

#entry_countInteger

Return the total count of line items across all versions and categories

Returns:

  • (Integer)

    total entry count



47
48
49
# File 'lib/philiprehberger/changelog_parser/changelog.rb', line 47

def entry_count
  @entries.sum { |e| e.categories.values.sum(&:size) }
end

#filter(category:) ⇒ Array<Hash>

Return all entries from a specific category across all versions.

Parameters:

  • category (String)

    the category to filter by (e.g., ‘Added’, ‘Fixed’)

Returns:

  • (Array<Hash>)

    matches with :version, :date, and :entry keys



186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/philiprehberger/changelog_parser/changelog.rb', line 186

def filter(category:)
  results = []

  @entries.each do |entry|
    next unless entry.categories.key?(category)

    entry.categories[category].each do |item|
      results << { version: entry.version, date: entry.date, entry: item }
    end
  end

  results
end

#latestVersionEntry?

Return the latest released version

Returns:



61
62
63
# File 'lib/philiprehberger/changelog_parser/changelog.rb', line 61

def latest
  @entries.reject { |e| e.version == 'Unreleased' }.first
end

#release(version_string, date:) ⇒ VersionEntry

Create a new released version from Unreleased

Parameters:

  • version_string (String)

    the version number

  • date (String)

    the release date (YYYY-MM-DD)

Returns:

Raises:



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/philiprehberger/changelog_parser/changelog.rb', line 83

def release(version_string, date:)
  unrel = unreleased
  raise Error, 'no Unreleased section found' unless unrel

  new_entry = VersionEntry.new(
    version: version_string,
    date: date,
    categories: unrel.categories.transform_values(&:dup)
  )

  unrel.categories.clear
  idx = @entries.index(unrel)
  @entries.insert(idx + 1, new_entry)
  new_entry
end

#remove(version_string, category, entry) ⇒ void

This method returns an undefined value.

Remove an entry from a version under a category

Parameters:

  • version_string (String)

    the version to remove from

  • category (String)

    the category

  • entry (String)

    the entry text to remove

Raises:

  • (Error)

    if the version or entry is not found



207
208
209
210
211
212
213
214
# File 'lib/philiprehberger/changelog_parser/changelog.rb', line 207

def remove(version_string, category, entry)
  ver = version(version_string)
  raise Error, "version #{version_string} not found" unless ver
  raise Error, "entry not found in #{version_string} [#{category}]" unless ver.categories[category]&.include?(entry)

  ver.categories[category].delete(entry)
  ver.categories.delete(category) if ver.categories[category].empty?
end

#search(query) ⇒ Array<Hash>

Search all entries for a keyword or pattern.

Parameters:

  • query (String, Regexp)

    the search term

Returns:

  • (Array<Hash>)

    matches with :version, :category, and :entry keys



133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/philiprehberger/changelog_parser/changelog.rb', line 133

def search(query)
  pattern = query.is_a?(Regexp) ? query : /#{Regexp.escape(query)}/i
  matches = []

  @entries.each do |entry|
    entry.categories.each do |category, items|
      items.each do |item|
        matches << { version: entry.version, category: category, entry: item } if pattern.match?(item)
      end
    end
  end

  matches
end

#since(version_string) ⇒ Hash<String, Array<String>>

Returns combined entries for all versions newer than the given version.

Parameters:

  • version_string (String)

    version to start from (exclusive)

Returns:

  • (Hash<String, Array<String>>)

    merged categories with entries

Raises:

  • (Error)

    if version is not found



121
122
123
124
125
126
127
# File 'lib/philiprehberger/changelog_parser/changelog.rb', line 121

def since(version_string)
  idx = @entries.index { |e| e.version == version_string }
  raise Philiprehberger::ChangelogParser::Error, "version not found: #{version_string}" unless idx

  range = @entries[0...idx].reject { |e| e.version == 'Unreleased' }
  merge_categories(range)
end

#to_json(*args) ⇒ String

Serialize the changelog as a JSON string

Parameters:

  • args (Array)

    arguments forwarded to Hash#to_json

Returns:

  • (String)

    the JSON string



236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/philiprehberger/changelog_parser/changelog.rb', line 236

def to_json(*args)
  require 'json'
  {
    title: @title,
    versions: @entries.map do |entry|
      {
        version: entry.version,
        date: entry.date,
        categories: entry.categories
      }
    end
  }.to_json(*args)
end

#to_markdownString

Render the changelog as markdown

Returns:

  • (String)

    the markdown string



253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
# File 'lib/philiprehberger/changelog_parser/changelog.rb', line 253

def to_markdown
  lines = []
  lines << "# #{@title}"
  lines << ''
  lines << @preamble unless @preamble.empty?

  @entries.each do |entry|
    lines << if entry.date
               "## [#{entry.version}] - #{entry.date}"
             else
               "## [#{entry.version}]"
             end
    lines << ''

    entry.categories.each do |category, items|
      lines << "### #{category}"
      lines << ''
      items.each { |item| lines << "- #{item}" }
      lines << ''
    end
  end

  lines.join("\n")
end

#unreleasedVersionEntry?

Return the unreleased entry

Returns:



54
55
56
# File 'lib/philiprehberger/changelog_parser/changelog.rb', line 54

def unreleased
  @entries.find { |e| e.version == 'Unreleased' }
end

#validateArray<String>

Validate the changelog for common issues.

Returns:

  • (Array<String>)

    warning messages (empty if valid)



151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/philiprehberger/changelog_parser/changelog.rb', line 151

def validate
  warnings = []
  released = @entries.reject { |e| e.version == 'Unreleased' }

  # Check for duplicate versions
  version_names = released.map(&:version)
  duplicates = version_names.select { |v| version_names.count(v) > 1 }.uniq
  warnings.concat(duplicates.map { |v| "duplicate version: #{v}" })

  # Check dates are in descending order
  dates = released.filter_map(&:date)
  dates.each_cons(2) do |newer, older|
    warnings << "date out of order: #{newer} before #{older}" if newer < older
  end

  # Check for empty released versions
  released.each do |entry|
    warnings << "empty version: #{entry.version}" if entry.empty?
  end

  warnings
end

#version(version_string) ⇒ VersionEntry?

Find a specific version entry

Parameters:

  • version_string (String)

    the version to find

Returns:



33
34
35
# File 'lib/philiprehberger/changelog_parser/changelog.rb', line 33

def version(version_string)
  @entries.find { |e| e.version == version_string }
end

#versionsArray<String>

Return all version strings

Returns:

  • (Array<String>)

    version strings



25
26
27
# File 'lib/philiprehberger/changelog_parser/changelog.rb', line 25

def versions
  @entries.map(&:version)
end

#write(path) ⇒ void

This method returns an undefined value.

Write the changelog to a file

Parameters:

  • path (String)

    the file path



178
179
180
# File 'lib/philiprehberger/changelog_parser/changelog.rb', line 178

def write(path)
  File.write(path, to_markdown)
end