reading 0.6.0 → 0.7.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.
- checksums.yaml +4 -4
- data/bin/reading +8 -8
- data/bin/readingfile +31 -0
- data/lib/reading/config.rb +115 -148
- data/lib/reading/errors.rb +11 -64
- data/lib/reading/item/time_length.rb +138 -0
- data/lib/reading/parsing/attributes/attribute.rb +26 -0
- data/lib/reading/parsing/attributes/author.rb +15 -0
- data/lib/reading/parsing/attributes/experiences/dates_and_head_transformer.rb +106 -0
- data/lib/reading/parsing/attributes/experiences/history_transformer.rb +452 -0
- data/lib/reading/parsing/attributes/experiences/spans_validator.rb +149 -0
- data/lib/reading/parsing/attributes/experiences.rb +27 -0
- data/lib/reading/parsing/attributes/genres.rb +16 -0
- data/lib/reading/parsing/attributes/notes.rb +22 -0
- data/lib/reading/parsing/attributes/rating.rb +17 -0
- data/lib/reading/parsing/attributes/shared.rb +62 -0
- data/lib/reading/parsing/attributes/title.rb +21 -0
- data/lib/reading/parsing/attributes/variants.rb +77 -0
- data/lib/reading/parsing/csv.rb +101 -0
- data/lib/reading/parsing/parser.rb +292 -0
- data/lib/reading/parsing/rows/column.rb +131 -0
- data/lib/reading/parsing/rows/comment.rb +26 -0
- data/lib/reading/parsing/rows/compact_planned.rb +30 -0
- data/lib/reading/parsing/rows/compact_planned_columns/head.rb +60 -0
- data/lib/reading/parsing/rows/regular.rb +33 -0
- data/lib/reading/parsing/rows/regular_columns/end_dates.rb +20 -0
- data/lib/reading/parsing/rows/regular_columns/genres.rb +20 -0
- data/lib/reading/parsing/rows/regular_columns/head.rb +45 -0
- data/lib/reading/parsing/rows/regular_columns/history.rb +143 -0
- data/lib/reading/parsing/rows/regular_columns/length.rb +35 -0
- data/lib/reading/parsing/rows/regular_columns/notes.rb +32 -0
- data/lib/reading/parsing/rows/regular_columns/rating.rb +15 -0
- data/lib/reading/parsing/rows/regular_columns/sources.rb +94 -0
- data/lib/reading/parsing/rows/regular_columns/start_dates.rb +35 -0
- data/lib/reading/parsing/transformer.rb +70 -0
- data/lib/reading/util/hash_compact_by_template.rb +1 -0
- data/lib/reading/util/hash_deep_merge.rb +1 -1
- data/lib/reading/util/hash_to_struct.rb +1 -0
- data/lib/reading/util/numeric_to_i_if_whole.rb +12 -0
- data/lib/reading/util/string_truncate.rb +13 -4
- data/lib/reading/version.rb +1 -1
- data/lib/reading.rb +18 -0
- metadata +58 -41
- data/lib/reading/attribute/all_attributes.rb +0 -83
- data/lib/reading/attribute/attribute.rb +0 -25
- data/lib/reading/attribute/experiences/dates_validator.rb +0 -94
- data/lib/reading/attribute/experiences/experiences_attribute.rb +0 -74
- data/lib/reading/attribute/experiences/progress_subattribute.rb +0 -48
- data/lib/reading/attribute/experiences/spans_subattribute.rb +0 -82
- data/lib/reading/attribute/variants/extra_info_subattribute.rb +0 -44
- data/lib/reading/attribute/variants/length_subattribute.rb +0 -45
- data/lib/reading/attribute/variants/series_subattribute.rb +0 -57
- data/lib/reading/attribute/variants/sources_subattribute.rb +0 -78
- data/lib/reading/attribute/variants/variants_attribute.rb +0 -69
- data/lib/reading/csv.rb +0 -67
- data/lib/reading/line.rb +0 -23
- data/lib/reading/row/blank_row.rb +0 -23
- data/lib/reading/row/compact_planned_row.rb +0 -130
- data/lib/reading/row/regular_row.rb +0 -94
- data/lib/reading/row/row.rb +0 -88
@@ -1,45 +0,0 @@
|
|
1
|
-
module Reading
|
2
|
-
class Row
|
3
|
-
class LengthSubattribute
|
4
|
-
using Util::HashArrayDeepFetch
|
5
|
-
|
6
|
-
private attr_reader :item_head, :bare_variant, :columns, :config
|
7
|
-
|
8
|
-
# @param bare_variant [String] the variant string before series / extra info.
|
9
|
-
# @param columns [Array<String>]
|
10
|
-
# @param config [Hash]
|
11
|
-
def initialize(bare_variant:, columns:, config:)
|
12
|
-
@bare_variant = bare_variant
|
13
|
-
@columns = columns
|
14
|
-
@config = config
|
15
|
-
end
|
16
|
-
|
17
|
-
def parse
|
18
|
-
in_variant = length_in(
|
19
|
-
bare_variant,
|
20
|
-
time_regex: config.deep_fetch(:csv, :regex, :time_length_in_variant),
|
21
|
-
pages_regex: config.deep_fetch(:csv, :regex, :pages_length_in_variant),
|
22
|
-
)
|
23
|
-
in_length = length_in(
|
24
|
-
columns[:length],
|
25
|
-
time_regex: config.deep_fetch(:csv, :regex, :time_length),
|
26
|
-
pages_regex: config.deep_fetch(:csv, :regex, :pages_length),
|
27
|
-
)
|
28
|
-
|
29
|
-
in_variant || in_length ||
|
30
|
-
(raise InvalidLengthError, "Missing length" unless columns[:length].blank?)
|
31
|
-
end
|
32
|
-
|
33
|
-
private
|
34
|
-
|
35
|
-
def length_in(str, time_regex:, pages_regex:)
|
36
|
-
return nil if str.blank?
|
37
|
-
|
38
|
-
time_length = str.strip.match(time_regex)&.captures&.first
|
39
|
-
return time_length unless time_length.nil?
|
40
|
-
|
41
|
-
str.strip.match(pages_regex)&.captures&.first&.to_i
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
@@ -1,57 +0,0 @@
|
|
1
|
-
module Reading
|
2
|
-
class Row
|
3
|
-
class SeriesSubattribute
|
4
|
-
using Util::HashArrayDeepFetch
|
5
|
-
|
6
|
-
private attr_reader :item_head, :variant_with_extras, :config
|
7
|
-
|
8
|
-
# @param item_head [String] see Row#item_heads for a definition.
|
9
|
-
# @param variant_with_extras [String] the full variant string.
|
10
|
-
# @param config [Hash]
|
11
|
-
def initialize(item_head:, variant_with_extras: nil, config:)
|
12
|
-
@item_head = item_head
|
13
|
-
@variant_with_extras = variant_with_extras
|
14
|
-
@config = config
|
15
|
-
end
|
16
|
-
|
17
|
-
def parse
|
18
|
-
(
|
19
|
-
Array(series(item_head)) +
|
20
|
-
Array(series(variant_with_extras))
|
21
|
-
).presence
|
22
|
-
end
|
23
|
-
|
24
|
-
def parse_head
|
25
|
-
series(item_head)
|
26
|
-
end
|
27
|
-
|
28
|
-
private
|
29
|
-
|
30
|
-
def template
|
31
|
-
config.deep_fetch(:item, :template, :variants, 0, :series).first
|
32
|
-
end
|
33
|
-
|
34
|
-
def series(str)
|
35
|
-
separated = str
|
36
|
-
.split(config.deep_fetch(:csv, :long_separator))
|
37
|
-
.map(&:strip)
|
38
|
-
.map(&:presence)
|
39
|
-
.compact
|
40
|
-
|
41
|
-
separated.delete_at(0) # everything before the series/extra info
|
42
|
-
|
43
|
-
separated.map { |str|
|
44
|
-
volume = str.match(config.deep_fetch(:csv, :regex, :series_volume))
|
45
|
-
prefix = "#{config.deep_fetch(:csv, :series_prefix)} "
|
46
|
-
|
47
|
-
if volume || str.start_with?(prefix)
|
48
|
-
{
|
49
|
-
name: str.delete_suffix(volume.to_s).delete_prefix(prefix) || template[:name],
|
50
|
-
volume: volume&.captures&.first&.to_i || template[:volume],
|
51
|
-
}
|
52
|
-
end
|
53
|
-
}.compact.presence
|
54
|
-
end
|
55
|
-
end
|
56
|
-
end
|
57
|
-
end
|
@@ -1,78 +0,0 @@
|
|
1
|
-
module Reading
|
2
|
-
class Row
|
3
|
-
class SourcesSubattribute
|
4
|
-
using Util::StringRemove
|
5
|
-
using Util::HashArrayDeepFetch
|
6
|
-
|
7
|
-
private attr_reader :item_head, :bare_variant, :config
|
8
|
-
|
9
|
-
# @param bare_variant [String] the variant string before series / extra info.
|
10
|
-
# @param config [Hash]
|
11
|
-
def initialize(bare_variant:, config:)
|
12
|
-
@bare_variant = bare_variant
|
13
|
-
@config = config
|
14
|
-
end
|
15
|
-
|
16
|
-
def parse
|
17
|
-
urls = sources_urls(bare_variant).map { |url|
|
18
|
-
{
|
19
|
-
name: url_name(url) || template.deep_fetch(:sources, 0, :name),
|
20
|
-
url: url,
|
21
|
-
}
|
22
|
-
}
|
23
|
-
|
24
|
-
names = sources_names(bare_variant).map { |name|
|
25
|
-
{
|
26
|
-
name: name,
|
27
|
-
url: template.deep_fetch(:sources, 0, :url),
|
28
|
-
}
|
29
|
-
}
|
30
|
-
|
31
|
-
(urls + names).presence
|
32
|
-
end
|
33
|
-
|
34
|
-
private
|
35
|
-
|
36
|
-
def template
|
37
|
-
@template ||= config.deep_fetch(:item, :template, :variants).first
|
38
|
-
end
|
39
|
-
|
40
|
-
def sources_urls(str)
|
41
|
-
str.scan(config.deep_fetch(:csv, :regex, :url))
|
42
|
-
end
|
43
|
-
|
44
|
-
# Turns everything that is not a source name (ISBN, source URL, length) into
|
45
|
-
# a separator, then splits by that separator and removes empty elements
|
46
|
-
# and format emojis. What's left is source names.
|
47
|
-
def sources_names(str)
|
48
|
-
not_names = [:isbn, :url, :time_length_in_variant, :pages_length_in_variant]
|
49
|
-
names_and_separators = str
|
50
|
-
|
51
|
-
not_names.each do |regex_type|
|
52
|
-
names_and_separators = names_and_separators.gsub(
|
53
|
-
config.deep_fetch(:csv, :regex, regex_type),
|
54
|
-
config.deep_fetch(:csv, :separator),
|
55
|
-
)
|
56
|
-
end
|
57
|
-
|
58
|
-
names_and_separators
|
59
|
-
.split(config.deep_fetch(:csv, :separator))
|
60
|
-
.map { |name| name.remove(/\A\s*#{config.deep_fetch(:csv, :regex, :formats)}\s*/) }
|
61
|
-
.map(&:strip)
|
62
|
-
.reject(&:empty?)
|
63
|
-
end
|
64
|
-
|
65
|
-
def url_name(url)
|
66
|
-
config
|
67
|
-
.deep_fetch(:item, :sources, :names_from_urls)
|
68
|
-
.each do |url_part, name|
|
69
|
-
if url.include?(url_part)
|
70
|
-
return name
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
config.deep_fetch(:item, :sources, :default_name_for_url)
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
78
|
-
end
|
@@ -1,69 +0,0 @@
|
|
1
|
-
require_relative "series_subattribute"
|
2
|
-
require_relative "sources_subattribute"
|
3
|
-
require_relative "length_subattribute"
|
4
|
-
require_relative "extra_info_subattribute"
|
5
|
-
|
6
|
-
module Reading
|
7
|
-
class Row
|
8
|
-
class VariantsAttribute < Attribute
|
9
|
-
using Util::HashArrayDeepFetch
|
10
|
-
|
11
|
-
def parse
|
12
|
-
sources_str = columns[:sources]&.presence || " "
|
13
|
-
|
14
|
-
format_as_separator = config.deep_fetch(:csv, :regex, :formats_split)
|
15
|
-
|
16
|
-
sources_str.split(format_as_separator).map { |variant_with_extras|
|
17
|
-
# without extra info or series
|
18
|
-
bare_variant = variant_with_extras
|
19
|
-
.split(config.deep_fetch(:csv, :long_separator))
|
20
|
-
.first
|
21
|
-
|
22
|
-
series_attr = SeriesSubattribute.new(item_head:, variant_with_extras:, config:)
|
23
|
-
sources_attr = SourcesSubattribute.new(bare_variant:, config:)
|
24
|
-
# Length, despite not being very complex, is still split out into a
|
25
|
-
# subattribute because it needs to be accessible to
|
26
|
-
# ExperiencesAttribute (more specifically SpansSubattribute) which
|
27
|
-
# uses length as a default value for amount.
|
28
|
-
length_attr = LengthSubattribute.new(bare_variant:, columns:, config:)
|
29
|
-
extra_info_attr = ExtraInfoSubattribute.new(item_head:, variant_with_extras:, config:)
|
30
|
-
|
31
|
-
variant =
|
32
|
-
{
|
33
|
-
format: format(bare_variant) || format(item_head) || template.fetch(:format),
|
34
|
-
series: series_attr.parse || template.fetch(:series),
|
35
|
-
sources: sources_attr.parse || template.fetch(:sources),
|
36
|
-
isbn: isbn(bare_variant) || template.fetch(:isbn),
|
37
|
-
length: length_attr.parse || template.fetch(:length),
|
38
|
-
extra_info: extra_info_attr.parse || template.fetch(:extra_info)
|
39
|
-
}
|
40
|
-
|
41
|
-
if variant != template
|
42
|
-
variant
|
43
|
-
else
|
44
|
-
nil
|
45
|
-
end
|
46
|
-
}.compact.presence
|
47
|
-
end
|
48
|
-
|
49
|
-
private
|
50
|
-
|
51
|
-
def template
|
52
|
-
@template ||= config.deep_fetch(:item, :template, :variants).first
|
53
|
-
end
|
54
|
-
|
55
|
-
def format(str)
|
56
|
-
emoji = str.match(/^#{config.deep_fetch(:csv, :regex, :formats)}/).to_s
|
57
|
-
config.deep_fetch(:item, :formats).key(emoji)
|
58
|
-
end
|
59
|
-
|
60
|
-
def isbn(str)
|
61
|
-
isbns = str.scan(config.deep_fetch(:csv, :regex, :isbn))
|
62
|
-
if isbns.count > 1
|
63
|
-
raise InvalidSourceError, "Only one ISBN/ASIN is allowed per item variant"
|
64
|
-
end
|
65
|
-
isbns[0]&.to_s
|
66
|
-
end
|
67
|
-
end
|
68
|
-
end
|
69
|
-
end
|
data/lib/reading/csv.rb
DELETED
@@ -1,67 +0,0 @@
|
|
1
|
-
# Used throughout, in other files.
|
2
|
-
require_relative "util/blank"
|
3
|
-
require_relative "util/string_remove"
|
4
|
-
require_relative "util/string_truncate"
|
5
|
-
require_relative "util/hash_to_struct"
|
6
|
-
require_relative "util/hash_deep_merge"
|
7
|
-
require_relative "util/hash_array_deep_fetch"
|
8
|
-
require_relative "util/hash_compact_by_template"
|
9
|
-
require_relative "errors"
|
10
|
-
|
11
|
-
# Used just here.
|
12
|
-
require_relative "config"
|
13
|
-
require_relative "line"
|
14
|
-
|
15
|
-
module Reading
|
16
|
-
class CSV
|
17
|
-
using Util::HashDeepMerge
|
18
|
-
using Util::HashArrayDeepFetch
|
19
|
-
using Util::HashToStruct
|
20
|
-
|
21
|
-
attr_reader :config
|
22
|
-
|
23
|
-
# @param feed [Object] the input source, which must respond to #each_line;
|
24
|
-
# if nil, the file at the given path is used.
|
25
|
-
# @param path [String] the path of the source file.
|
26
|
-
# @param config [Hash] a custom config which overrides the defaults,
|
27
|
-
# e.g. { errors: { styling: :html } }
|
28
|
-
def initialize(feed = nil, path: nil, config: {})
|
29
|
-
if feed.nil? && path.nil?
|
30
|
-
raise ArgumentError, "No file given to load."
|
31
|
-
end
|
32
|
-
|
33
|
-
if path
|
34
|
-
if !File.exist?(path)
|
35
|
-
raise FileError, "File not found! #{@path}"
|
36
|
-
elsif File.directory?(path)
|
37
|
-
raise FileError, "The reading log must be a file, but the path given is a directory: #{@path}"
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
@feed = feed
|
42
|
-
@path = path
|
43
|
-
@config ||= Config.new(config).hash
|
44
|
-
end
|
45
|
-
|
46
|
-
# Parses a CSV reading log into item data (an array of Structs).
|
47
|
-
# For what the Structs look like, see the Hash at @default_config[:item][:template]
|
48
|
-
# in config.rb. The Structs are identical in structure to that Hash (with
|
49
|
-
# every inner Hash replaced with a Struct).
|
50
|
-
# @return [Array<Struct>] an array of Structs like the template in config.rb
|
51
|
-
def parse
|
52
|
-
feed = @feed || File.open(@path)
|
53
|
-
items = []
|
54
|
-
|
55
|
-
feed.each_line do |string|
|
56
|
-
line = Line.new(string, self)
|
57
|
-
row = line.to_row
|
58
|
-
|
59
|
-
items += row.parse
|
60
|
-
end
|
61
|
-
|
62
|
-
items.map(&:to_struct)
|
63
|
-
ensure
|
64
|
-
feed&.close if feed.respond_to?(:close)
|
65
|
-
end
|
66
|
-
end
|
67
|
-
end
|
data/lib/reading/line.rb
DELETED
@@ -1,23 +0,0 @@
|
|
1
|
-
require_relative "row/compact_planned_row"
|
2
|
-
require_relative "row/blank_row"
|
3
|
-
require_relative "row/regular_row"
|
4
|
-
|
5
|
-
module Reading
|
6
|
-
# A bridge between rows as strings and as parsable Rows, used whenever the
|
7
|
-
# context of the line in the CSV is needed, e.g. converting a line to a Row,
|
8
|
-
# or adding a CSV line to a Row parsing error.
|
9
|
-
class Line
|
10
|
-
attr_reader :string, :csv
|
11
|
-
|
12
|
-
def initialize(string, csv)
|
13
|
-
@string = string.dup.force_encoding(Encoding::UTF_8).strip
|
14
|
-
@csv = csv
|
15
|
-
end
|
16
|
-
|
17
|
-
def to_row
|
18
|
-
return CompactPlannedRow.new(self) if CompactPlannedRow.match?(self)
|
19
|
-
return BlankRow.new(self) if BlankRow.match?(self)
|
20
|
-
RegularRow.new(self)
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
@@ -1,23 +0,0 @@
|
|
1
|
-
require_relative "row"
|
2
|
-
|
3
|
-
module Reading
|
4
|
-
# An empty or commented-out row. A null object which returns an empty array.
|
5
|
-
class BlankRow < Row
|
6
|
-
using Util::HashArrayDeepFetch
|
7
|
-
|
8
|
-
# Whether the given CSV line is a blank row.
|
9
|
-
# @param line [Reading::Line]
|
10
|
-
# @return [Boolean]
|
11
|
-
def self.match?(line)
|
12
|
-
comment_char = line.csv.config.deep_fetch(:csv, :comment_character)
|
13
|
-
|
14
|
-
line.string.strip.empty? ||
|
15
|
-
line.string.strip.start_with?(comment_char)
|
16
|
-
end
|
17
|
-
|
18
|
-
# Overrides Row#parse.
|
19
|
-
def parse
|
20
|
-
[]
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
@@ -1,130 +0,0 @@
|
|
1
|
-
require_relative "row"
|
2
|
-
require "debug"
|
3
|
-
|
4
|
-
module Reading
|
5
|
-
# Parses a row of compactly listed planned items into an array of hashes of
|
6
|
-
# item data.
|
7
|
-
class CompactPlannedRow < Row
|
8
|
-
using Util::StringRemove
|
9
|
-
using Util::HashDeepMerge
|
10
|
-
using Util::HashArrayDeepFetch
|
11
|
-
|
12
|
-
# Whether the given CSV line is a compact planned row.
|
13
|
-
# @param line [Reading::Line]
|
14
|
-
# @return [Boolean]
|
15
|
-
def self.match?(line)
|
16
|
-
comment_char = line.csv.config.deep_fetch(:csv, :comment_character)
|
17
|
-
|
18
|
-
line.string.strip.start_with?(comment_char) &&
|
19
|
-
line.string.match?(line.csv.config.deep_fetch(:csv, :regex, :compact_planned_row_start))
|
20
|
-
end
|
21
|
-
|
22
|
-
private
|
23
|
-
|
24
|
-
def skip?
|
25
|
-
config.deep_fetch(:csv, :skip_compact_planned)
|
26
|
-
end
|
27
|
-
|
28
|
-
def before_parse
|
29
|
-
to_ignore = config.deep_fetch(:csv, :regex, :compact_planned_ignored_chars)
|
30
|
-
start_regex = config.deep_fetch(:csv, :regex, :compact_planned_row_start)
|
31
|
-
|
32
|
-
string_without_ignored_chars = string.remove_all(to_ignore)
|
33
|
-
start = string_without_ignored_chars.match(start_regex)
|
34
|
-
|
35
|
-
@genres = Array(start[:genres]&.downcase&.strip&.split(",")&.map(&:strip))
|
36
|
-
@sources = sources(start[:sources])
|
37
|
-
@row_without_genre = string_without_ignored_chars.remove(start.to_s)
|
38
|
-
end
|
39
|
-
|
40
|
-
def string_to_be_split_by_format_emojis
|
41
|
-
@row_without_genre
|
42
|
-
end
|
43
|
-
|
44
|
-
def item_hash(item_head)
|
45
|
-
item_match = item_head.match(config.deep_fetch(:csv, :regex, :compact_planned_item))
|
46
|
-
unless item_match
|
47
|
-
raise InvalidHeadError, "Title missing after #{item_head} in compact planned row"
|
48
|
-
end
|
49
|
-
|
50
|
-
author = AuthorAttribute.new(item_head: item_match[:author_title], config:).parse
|
51
|
-
|
52
|
-
begin
|
53
|
-
title = TitleAttribute.new(item_head: item_match[:author_title], config:).parse
|
54
|
-
rescue InvalidHeadError
|
55
|
-
raise InvalidHeadError, "Title missing after #{item_head} in compact planned row"
|
56
|
-
end
|
57
|
-
|
58
|
-
if item_match[:sources_column]
|
59
|
-
if item_match[:sources_column].include?(config.deep_fetch(:csv, :column_separator))
|
60
|
-
raise InvalidSourceError, "Too many columns (only Sources allowed) " \
|
61
|
-
"after #{item_head} in compact planned row"
|
62
|
-
end
|
63
|
-
|
64
|
-
variants_attr = VariantsAttribute.new(
|
65
|
-
item_head: item_match[:format_emoji] + item_match[:author_title],
|
66
|
-
columns: { sources: item_match[:sources_column], length: nil },
|
67
|
-
config:,
|
68
|
-
)
|
69
|
-
variants = variants_attr.parse
|
70
|
-
else
|
71
|
-
variants = [parse_variant(item_match)]
|
72
|
-
end
|
73
|
-
|
74
|
-
template.deep_merge(
|
75
|
-
author: author || template.fetch(:author),
|
76
|
-
title: title,
|
77
|
-
genres: @genres.presence || template.fetch(:genres),
|
78
|
-
variants:,
|
79
|
-
)
|
80
|
-
end
|
81
|
-
|
82
|
-
def template
|
83
|
-
@template ||= config.deep_fetch(:item, :template)
|
84
|
-
end
|
85
|
-
|
86
|
-
def parse_variant(item_match)
|
87
|
-
item_head = item_match[:format_emoji] + item_match[:author_title]
|
88
|
-
series_attr = SeriesSubattribute.new(item_head:, config:)
|
89
|
-
extra_info_attr = ExtraInfoSubattribute.new(item_head:, config:)
|
90
|
-
sources = (@sources + sources(item_match[:sources])).uniq.presence
|
91
|
-
|
92
|
-
{
|
93
|
-
format: format(item_match[:format_emoji]),
|
94
|
-
series: series_attr.parse_head || template.deep_fetch(:variants, 0, :series),
|
95
|
-
sources: sources || template.deep_fetch(:variants, 0, :sources),
|
96
|
-
isbn: template.deep_fetch(:variants, 0, :isbn),
|
97
|
-
length: template.deep_fetch(:variants, 0, :length),
|
98
|
-
extra_info: extra_info_attr.parse_head || template.deep_fetch(:variants, 0, :extra_info),
|
99
|
-
}
|
100
|
-
end
|
101
|
-
|
102
|
-
def format(format_emoji)
|
103
|
-
config.deep_fetch(:item, :formats).key(format_emoji)
|
104
|
-
end
|
105
|
-
|
106
|
-
def sources(sources_str)
|
107
|
-
return [] if sources_str.nil?
|
108
|
-
|
109
|
-
sources_str
|
110
|
-
.split(config.deep_fetch(:csv, :compact_planned_source_prefix))
|
111
|
-
.map { |source| source.remove(/\s*,\s*/) }
|
112
|
-
.map(&:strip)
|
113
|
-
.reject(&:empty?)
|
114
|
-
.map { |source_name|
|
115
|
-
if valid_url?(source_name)
|
116
|
-
source_name = source_name.chop if source_name.chars.last == "/"
|
117
|
-
{ name: config.deep_fetch(:item, :sources, :default_name_for_url),
|
118
|
-
url: source_name }
|
119
|
-
else
|
120
|
-
{ name: source_name,
|
121
|
-
url: nil }
|
122
|
-
end
|
123
|
-
}
|
124
|
-
end
|
125
|
-
|
126
|
-
def valid_url?(str)
|
127
|
-
str&.match?(/http[^\s,]+/)
|
128
|
-
end
|
129
|
-
end
|
130
|
-
end
|
@@ -1,94 +0,0 @@
|
|
1
|
-
require_relative "row"
|
2
|
-
require_relative "../attribute/all_attributes"
|
3
|
-
|
4
|
-
module Reading
|
5
|
-
# Parses a normal CSV row into an array of hashes of item data. Typically
|
6
|
-
# a normal row describes one item and so it's parsed into an array containing
|
7
|
-
# a single hash, but it's also possible for a row to describe multiple items.
|
8
|
-
class RegularRow < Row
|
9
|
-
using Util::HashArrayDeepFetch
|
10
|
-
|
11
|
-
private attr_reader :columns, :attribute_classes
|
12
|
-
|
13
|
-
private
|
14
|
-
|
15
|
-
def after_initialize
|
16
|
-
set_attribute_classes
|
17
|
-
end
|
18
|
-
|
19
|
-
def before_parse
|
20
|
-
set_columns
|
21
|
-
ensure_head_column_present
|
22
|
-
end
|
23
|
-
|
24
|
-
def string_to_be_split_by_format_emojis
|
25
|
-
columns[:head]
|
26
|
-
end
|
27
|
-
|
28
|
-
def set_attribute_classes
|
29
|
-
@attribute_classes ||= config.deep_fetch(:item, :template).map { |attribute_name, _default|
|
30
|
-
attribute_name_camelcase = attribute_name.to_s.split("_").map(&:capitalize).join
|
31
|
-
attribute_class_name = "#{attribute_name_camelcase}Attribute"
|
32
|
-
attribute_class = self.class.const_get(attribute_class_name)
|
33
|
-
|
34
|
-
[attribute_name, attribute_class]
|
35
|
-
}.to_h
|
36
|
-
.merge(custom_attribute_classes)
|
37
|
-
end
|
38
|
-
|
39
|
-
def custom_attribute_classes
|
40
|
-
numeric = custom_attribute_classes_of_type(:numeric) do |value|
|
41
|
-
Float(value, exception: false)
|
42
|
-
end
|
43
|
-
|
44
|
-
text = custom_attribute_classes_of_type(:text) do |value|
|
45
|
-
value
|
46
|
-
end
|
47
|
-
|
48
|
-
(numeric + text).to_h
|
49
|
-
end
|
50
|
-
|
51
|
-
def custom_attribute_classes_of_type(type, &process_value)
|
52
|
-
config.deep_fetch(:csv, :"custom_#{type}_columns").map { |attribute, _default_value|
|
53
|
-
custom_class = Class.new(Attribute)
|
54
|
-
|
55
|
-
custom_class.define_method(:parse) do
|
56
|
-
value = columns[attribute.to_sym]&.strip&.presence
|
57
|
-
process_value.call(value)
|
58
|
-
end
|
59
|
-
|
60
|
-
[attribute.to_sym, custom_class]
|
61
|
-
}
|
62
|
-
end
|
63
|
-
|
64
|
-
def set_columns
|
65
|
-
@columns = (
|
66
|
-
config.deep_fetch(:csv, :enabled_columns) +
|
67
|
-
config.deep_fetch(:csv, :custom_numeric_columns).keys +
|
68
|
-
config.deep_fetch(:csv, :custom_text_columns).keys
|
69
|
-
)
|
70
|
-
.zip(string.split(config.deep_fetch(:csv, :column_separator)))
|
71
|
-
.to_h
|
72
|
-
end
|
73
|
-
|
74
|
-
def ensure_head_column_present
|
75
|
-
if columns[:head].nil? || columns[:head].strip.empty?
|
76
|
-
raise InvalidHeadError, "The Head column must not be blank"
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
|
-
def item_hash(item_head)
|
81
|
-
config
|
82
|
-
.deep_fetch(:item, :template)
|
83
|
-
.merge(config.deep_fetch(:csv, :custom_numeric_columns))
|
84
|
-
.merge(config.deep_fetch(:csv, :custom_text_columns))
|
85
|
-
.map { |attribute_name, default_value|
|
86
|
-
attribute_class = attribute_classes.fetch(attribute_name)
|
87
|
-
attribute_parser = attribute_class.new(item_head:, columns:, config:)
|
88
|
-
parsed = attribute_parser.parse
|
89
|
-
|
90
|
-
[attribute_name, parsed || default_value]
|
91
|
-
}.to_h
|
92
|
-
end
|
93
|
-
end
|
94
|
-
end
|
data/lib/reading/row/row.rb
DELETED
@@ -1,88 +0,0 @@
|
|
1
|
-
module Reading
|
2
|
-
# A base class that contains behaviors common to ___Row classes.
|
3
|
-
class Row
|
4
|
-
using Util::StringRemove
|
5
|
-
using Util::HashArrayDeepFetch
|
6
|
-
using Util::HashCompactByTemplate
|
7
|
-
|
8
|
-
private attr_reader :line
|
9
|
-
|
10
|
-
# @param line [Reading::Line] the Line that this Row represents.
|
11
|
-
def initialize(line)
|
12
|
-
@line = line
|
13
|
-
|
14
|
-
after_initialize
|
15
|
-
end
|
16
|
-
|
17
|
-
# Parses a CSV row into an array of hashes of item data. How this is done
|
18
|
-
# depends on how the template methods (further below) are implemented in
|
19
|
-
# subclasses of Row.
|
20
|
-
# @return [Array<Hash>] an array of hashes like the template in config.rb
|
21
|
-
def parse
|
22
|
-
return [] if skip?
|
23
|
-
|
24
|
-
before_parse
|
25
|
-
|
26
|
-
items = item_heads.map { |item_head|
|
27
|
-
item_hash(item_head)
|
28
|
-
.compact_by(template: config.deep_fetch(:item, :template))
|
29
|
-
}.compact
|
30
|
-
|
31
|
-
items
|
32
|
-
|
33
|
-
rescue Reading::Error => e
|
34
|
-
e.handle(line:)
|
35
|
-
[]
|
36
|
-
end
|
37
|
-
|
38
|
-
private
|
39
|
-
|
40
|
-
def string
|
41
|
-
@line.string
|
42
|
-
end
|
43
|
-
|
44
|
-
def config
|
45
|
-
@line.csv.config
|
46
|
-
end
|
47
|
-
|
48
|
-
# A "head" is a string in the Head column containing a chunk of item
|
49
|
-
# information, starting with a format emoji. A typical row describes one
|
50
|
-
# item and so contains one head, but a row describing multiple items (with
|
51
|
-
# multiple heads in the Head column) is possible. Also, a row of compact
|
52
|
-
# planned items is essentially a list of heads, though with different
|
53
|
-
# elements than a normal row's head.
|
54
|
-
# @return [Array<String>]
|
55
|
-
def item_heads
|
56
|
-
string_to_be_split_by_format_emojis
|
57
|
-
.split(config.deep_fetch(:csv, :regex, :formats_split))
|
58
|
-
.tap { |item_heads|
|
59
|
-
item_heads.first.remove!(config.deep_fetch(:csv, :regex, :dnf))
|
60
|
-
item_heads.first.remove!(config.deep_fetch(:csv, :regex, :progress))
|
61
|
-
}
|
62
|
-
.map { |item_head| item_head.strip }
|
63
|
-
.partition { |item_head| item_head.match?(/\A#{config.deep_fetch(:csv, :regex, :formats)}/) }
|
64
|
-
.reject(&:empty?)
|
65
|
-
.first
|
66
|
-
end
|
67
|
-
|
68
|
-
# Below: template methods that can (or must) be overridden.
|
69
|
-
|
70
|
-
def after_initialize
|
71
|
-
end
|
72
|
-
|
73
|
-
def before_parse
|
74
|
-
end
|
75
|
-
|
76
|
-
def skip?
|
77
|
-
false
|
78
|
-
end
|
79
|
-
|
80
|
-
def string_to_be_split_by_format_emojis
|
81
|
-
raise NotImplementedError, "#{self.class} should have implemented #{__method__}"
|
82
|
-
end
|
83
|
-
|
84
|
-
def item_hash(item_head)
|
85
|
-
raise NotImplementedError, "#{self.class} should have implemented #{__method__}"
|
86
|
-
end
|
87
|
-
end
|
88
|
-
end
|