Commit 6f10f768 authored by Alex Kalderimis's avatar Alex Kalderimis

Remove exceptional control flow

This introduces a new intermediate object, `Block` to manage the parser
state.

Co-authored-by: @dbalexandre
parent 1b2a3353
...@@ -15,7 +15,7 @@ module Gitlab ...@@ -15,7 +15,7 @@ module Gitlab
\s* \s*
^(?<delim>#{DELIM})[ \t]*(?<lang>\S*) # opening front matter marker (optional language specifier) ^(?<delim>#{DELIM})[ \t]*(?<lang>\S*) # opening front matter marker (optional language specifier)
\s* \s*
^(?<front_matter>.*?) # front matter (not greedy) ^(?<front_matter>.*?) # front matter block content (not greedy)
\s* \s*
^(\k<delim> | \.{3}) # closing front matter marker ^(\k<delim> | \.{3}) # closing front matter marker
\s* \s*
......
...@@ -3,19 +3,6 @@ ...@@ -3,19 +3,6 @@
module Gitlab module Gitlab
module WikiPages module WikiPages
class FrontMatterParser class FrontMatterParser
# A ParseResult contains the de-serialized front-matter, the stripped
# content, and maybe an error, explaining why there is no front-matter.
ParseResult = Struct.new(:front_matter, :content, :reason, :error, keyword_init: true)
class NoFrontMatter < StandardError
attr_reader :reason
def initialize(reason)
super
@reason = reason
end
end
FEATURE_FLAG = :wiki_front_matter FEATURE_FLAG = :wiki_front_matter
# We limit the maximum length of text we are prepared to parse as YAML, to # We limit the maximum length of text we are prepared to parse as YAML, to
...@@ -29,9 +16,21 @@ module Gitlab ...@@ -29,9 +16,21 @@ module Gitlab
SLUG_LINE_LENGTH = (4 + Gitlab::WikiPages::MAX_DIRECTORY_BYTES + 1 + Gitlab::WikiPages::MAX_TITLE_BYTES) SLUG_LINE_LENGTH = (4 + Gitlab::WikiPages::MAX_DIRECTORY_BYTES + 1 + Gitlab::WikiPages::MAX_TITLE_BYTES)
MAX_FRONT_MATTER_LENGTH = (8 + Gitlab::WikiPages::MAX_TITLE_BYTES) + 7 + (SLUG_LINE_LENGTH * MAX_SLUGS) MAX_FRONT_MATTER_LENGTH = (8 + Gitlab::WikiPages::MAX_TITLE_BYTES) + 7 + (SLUG_LINE_LENGTH * MAX_SLUGS)
ParseError = Class.new(StandardError)
class Result
attr_reader :front_matter, :content, :reason, :error
def initialize(content:, front_matter: {}, reason: nil, error: nil)
@content = content
@front_matter = front_matter.freeze
@reason = reason
@error = error
end
end
# @param [String] wiki_content # @param [String] wiki_content
# @param [FeatureGate] feature_gate The scope for feature availability # @param [FeatureGate] feature_gate The scope for feature availability
# (usually a project)
def initialize(wiki_content, feature_gate) def initialize(wiki_content, feature_gate)
@wiki_content = wiki_content @wiki_content = wiki_content
@feature_gate = feature_gate @feature_gate = feature_gate
...@@ -42,52 +41,78 @@ module Gitlab ...@@ -42,52 +41,78 @@ module Gitlab
end end
def parse def parse
ParseResult.new(front_matter: extract_front_matter, content: strip_front_matter) return empty_result unless enabled? && wiki_content.present?
rescue NoFrontMatter => e return empty_result(block.error) unless block.valid?
ParseResult.new(front_matter: {}, content: wiki_content, reason: e.reason, error: e.cause)
Result.new(front_matter: block.data, content: strip_front_matter_block)
rescue ParseError => error
empty_result(:parse_error, error)
end end
private class Block
include Gitlab::Utils::StrongMemoize
attr_reader :wiki_content, :feature_gate def initialize(delim = nil, lang = '', text = nil)
@lang = lang.downcase.presence || Gitlab::FrontMatter::DELIM_LANG[delim]
@text = text
end
def extract_front_matter def data
ensure_enabled! @data ||= YAML.safe_load(text, symbolize_names: true)
front_matter, lang = extract rescue Psych::DisallowedClass, Psych::SyntaxError => error
front_matter = parse_string(front_matter, lang) raise ParseError, error.message
validate(front_matter) end
front_matter def valid?
end error.nil?
end
def parse_string(source, lang) def error
raise NoFrontMatter, :not_yaml unless lang == 'yaml' strong_memoize(:error) { no_match? || too_long? || not_yaml? || not_mapping? }
end
YAML.safe_load(source, symbolize_names: true) private
rescue Psych::DisallowedClass, Psych::SyntaxError
raise NoFrontMatter, :parse_error attr_reader :lang, :text
end
def no_match?
:no_match if text.nil?
end
def not_yaml?
:not_yaml if lang != 'yaml'
end
def too_long?
:too_long if text.size > MAX_FRONT_MATTER_LENGTH
end
def validate(parsed) def not_mapping?
raise NoFrontMatter, :not_mapping unless Hash === parsed :not_mapping unless data.is_a?(Hash)
end
end end
def extract private
raise NoFrontMatter, :no_content unless wiki_content.present?
attr_reader :wiki_content, :feature_gate
def empty_result(reason = nil, error = nil)
Result.new(content: wiki_content, reason: reason, error: error)
end
match = Gitlab::FrontMatter::PATTERN.match(wiki_content) if wiki_content.present? def enabled?
raise NoFrontMatter, :no_pattern_match unless match self.class.enabled?(feature_gate)
raise NoFrontMatter, :too_long if match[:front_matter].size > MAX_FRONT_MATTER_LENGTH end
lang = match[:lang].downcase.presence || Gitlab::FrontMatter::DELIM_LANG[match[:delim]] def block
[match[:front_matter], lang] @block ||= parse_front_matter_block
end end
def ensure_enabled! def parse_front_matter_block
raise NoFrontMatter, :feature_flag_disabled unless self.class.enabled?(feature_gate) wiki_content.match(Gitlab::FrontMatter::PATTERN) { |m| Block.new(*m.captures) } || Block.new
end end
def strip_front_matter def strip_front_matter_block
wiki_content.gsub(Gitlab::FrontMatter::PATTERN, '') wiki_content.gsub(Gitlab::FrontMatter::PATTERN, '')
end end
end end
......
...@@ -45,7 +45,13 @@ describe Gitlab::WikiPages::FrontMatterParser do ...@@ -45,7 +45,13 @@ describe Gitlab::WikiPages::FrontMatterParser do
context 'there is no content' do context 'there is no content' do
let(:raw_content) { '' } let(:raw_content) { '' }
it { is_expected.to have_attributes(reason: :no_content) } it do
is_expected.to have_attributes(
front_matter: {},
content: raw_content,
error: be_nil
)
end
end end
context 'there is no front_matter' do context 'there is no front_matter' do
...@@ -53,7 +59,7 @@ describe Gitlab::WikiPages::FrontMatterParser do ...@@ -53,7 +59,7 @@ describe Gitlab::WikiPages::FrontMatterParser do
it { is_expected.to have_attributes(front_matter: be_empty, content: raw_content) } it { is_expected.to have_attributes(front_matter: be_empty, content: raw_content) }
it { is_expected.to have_attributes(reason: :no_pattern_match) } it { is_expected.to have_attributes(reason: :no_match) }
end end
context 'the feature flag is disabled' do context 'the feature flag is disabled' do
...@@ -265,12 +271,4 @@ describe Gitlab::WikiPages::FrontMatterParser do ...@@ -265,12 +271,4 @@ describe Gitlab::WikiPages::FrontMatterParser do
it { is_expected.to have_attributes(reason: :not_mapping) } it { is_expected.to have_attributes(reason: :not_mapping) }
end end
end end
describe '#strip_front_matter' do
let(:raw_content) { with_front_matter }
it 'removes the front-matter from the content' do
expect(subject.send(:strip_front_matter)).to eq(content + "\n")
end
end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment