datanorm 0.0.1

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.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +154 -0
  3. data/lib/datanorm/document.rb +55 -0
  4. data/lib/datanorm/documents/assemble.rb +43 -0
  5. data/lib/datanorm/documents/assembles/price.rb +85 -0
  6. data/lib/datanorm/documents/assembles/product.rb +176 -0
  7. data/lib/datanorm/documents/preprocess.rb +36 -0
  8. data/lib/datanorm/documents/preprocesses/cache.rb +76 -0
  9. data/lib/datanorm/documents/preprocesses/process.rb +80 -0
  10. data/lib/datanorm/file.rb +65 -0
  11. data/lib/datanorm/header.rb +46 -0
  12. data/lib/datanorm/headers/v4/date.rb +39 -0
  13. data/lib/datanorm/headers/v4/version.rb +36 -0
  14. data/lib/datanorm/headers/v5/date.rb +25 -0
  15. data/lib/datanorm/headers/v5/version.rb +36 -0
  16. data/lib/datanorm/helpers/filename.rb +20 -0
  17. data/lib/datanorm/helpers/utf8.rb +20 -0
  18. data/lib/datanorm/lines/base.rb +67 -0
  19. data/lib/datanorm/lines/parse.rb +33 -0
  20. data/lib/datanorm/lines/v4/dimension.rb +44 -0
  21. data/lib/datanorm/lines/v4/extra.rb +55 -0
  22. data/lib/datanorm/lines/v4/parse.rb +42 -0
  23. data/lib/datanorm/lines/v4/price.rb +120 -0
  24. data/lib/datanorm/lines/v4/priceset.rb +42 -0
  25. data/lib/datanorm/lines/v4/product.rb +90 -0
  26. data/lib/datanorm/lines/v4/text.rb +31 -0
  27. data/lib/datanorm/lines/v5/dimension.rb +22 -0
  28. data/lib/datanorm/lines/v5/parse.rb +29 -0
  29. data/lib/datanorm/lines/v5/price.rb +27 -0
  30. data/lib/datanorm/lines/v5/product.rb +42 -0
  31. data/lib/datanorm/lines/v5/text.rb +30 -0
  32. data/lib/datanorm/logger.rb +15 -0
  33. data/lib/datanorm/logging.rb +27 -0
  34. data/lib/datanorm/progress.rb +26 -0
  35. data/lib/datanorm/version.rb +5 -0
  36. data/lib/datanorm.rb +49 -0
  37. metadata +158 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f44ab02c4041e106b7e120a1eae889c7d3e1384c4dbaedd6adea4bc86a0d1895
4
+ data.tar.gz: 338c389dded4de592d34d9730080bbf800052c32359a2294fea1aaa45d25b99b
5
+ SHA512:
6
+ metadata.gz: 342576af02ba6ae033e97090908944045896e2bd2df1e496ec83e08835945e737051a62d139300dd10f1d9711ac1e88901e94715e0d060024390975ed2e10e39
7
+ data.tar.gz: 535491f2403475fb0227e6e4e81db3b4c43bb512b70640d04852a110359c74816d1ec3b02ab959a7d83a9fed62269a54a21401efb7db1a24025e629d3c7f271b
data/README.md ADDED
@@ -0,0 +1,154 @@
1
+ # About Datanorm
2
+
3
+ Datanorm is a German legacy file format for serialization of B2B product data (stock lists) optimized for floppy disks and monospace needle printers. Version 4 was published in 1994 and version 5 in 1999, since then there has been no change in the format. Earlier versions date back to 1986 and are practically extinct.
4
+
5
+ 30 years later, it is still the de-facto standard used by German product suppliers (especially electricity and plumbing) to communicate to their business clients, which products can be bought at which price.
6
+
7
+ Disadvantages:
8
+
9
+ * One line of text (e.g. a product name or description) is limited to 40 characters. This is because of monospace fonts where a quotation or an invoice would have column limitations. There is no reliable way to convert this into flowing text (mostly because of bullet lists).
10
+
11
+ * One product can only have one price.
12
+ So the suppliers will typically export multiple Datanorm files to their clients: one with list prices, one with discounted buying prices and one with recommended selling prices. Or, they'll provide a second file that only contains various prices.
13
+
14
+ * The file encoding is not UTF-8 but `CP850`, common in DOS in Western Europe.
15
+
16
+ * There are cross-references between lines (and even parts of lines) within a single Datanorm file, that make parsing very complicated and inefficient (Datanorm files are commonly between 1 MB and 3 GB large).
17
+
18
+ * Version 4, still most commonly used, has special quirks, such as only allowing a product quantity unit of 1, 10, 100 or 1000. So you cannot have a product sold in packs of, say 25.
19
+
20
+ * One line in the file for prices "P" may actually be a set of prices for up to three same or different products. Thus data that belongs together separated by new lines at seemingly random places.
21
+
22
+ * Over the years, people started working around the standard to overcome its limitations. In other words, every company that exports Datanorm, uses data fields in different ways to communicate different things and many structure the content of the file differently. For example, one file for creating products, one for amending existing products and one for doing both. Another example is to use product descriptions and product category descriptions interchangeably, and using free text fields to override normalized values that are wrong.
23
+
24
+ * To my knowledge, there is no documentation publicly available on the Internet. Maybe old books in some library have it.
25
+
26
+ If you were ever wondering why Germany has so much bureaucracy, it's because they like to cling on to things.
27
+
28
+ ### File types
29
+
30
+ There are various files with various extensions, but the main ones are those that are called `DATANORM.001`, `DATANORM.002`, etc. They have this convention because those files used to be on one floppy disk each. Nowadays you only have `DATANORM.001`, which can be several GB large.
31
+
32
+ Other file types are `.RAB` (for discount groups) and `.WRG` (for product categories), but we don't support them yet (not difficult to implement, though).
33
+
34
+ ### Main Datanorm file format
35
+
36
+ In Datanorm, *one line* in the file represents one record. The most common ones are:
37
+
38
+ * The very first line in the file is the header that identifies the Datanorm version (4 or 5) and a date (indicating when the prices are in effect). In V4 the fields are separated by fixed-length and in V5 they are separated by semicolon.
39
+
40
+ * Sometimes there is a `V` record as second line of the file, it is additional information such as the number of a printed catalogue.
41
+
42
+ * One `A` record represent one product. It is identified by a product ID that is usually unique per one supplier (and thus, it is unique within one Datanorm file of a supplier). So there is only one `A` record with a given ID in one file. This is the most important kind of record and holds product name, short description and price. It can be located anywhere in the file and can reference multiple other records (that is, lines anywhere else in the file), such as records with long text descriptions.
43
+
44
+ * The `B` record is mostly relevant in V4 and holds additional product data, such as an EAN code. In V5 it is used as a DELETE statement for a product (so that the supplier can tell the business client that this product is now or soon deprecated), but we don't support really that feature. The `B` record has the same ID as the `A` record, so you know that they belong together. There is only one `B` record per product and within a Datanorm file, the `B` record is usually located one line below the `A` record.
45
+
46
+ * The `D` record represents one line of the description of one product. So you have multiple `D` records where each of them has the same ID as the product. Additionally, each record holds one line number, so you know which of those records represents which line of the product description. To complicate things further, each `D` records has two text fields (of course each limited to 40 characters), so most people put two lines of text into one `D` record (say, records for line 1, 3, and 5, which in reality represent the product description lines 1-6). The `D` records are usually located below or above the `A` record.
47
+
48
+ * `T` records were originally meant to be long texts that similar products could reference to. So, in addition to the description `D` for one single product, each product could additionally reference one `T` text that it might share with other products. But in practice they are not shared and `T` is often used instead of `D`. In other words, sometimes every single product has one single set of `T` records just for that one product. What makes `T` records complicated, in terms of file processing, is that each set of `T` records has a unique ID that is not related to any product. Rather, a product will reference that (made-up) text record set ID to indicate that those `T` records hold the long text description for a product. But the `T` records could be anywhere else in the file, so it's hard to parse.
49
+
50
+ * Usually, one `A` record holds one price for that product. But there also exist so called `P` records (one for *multiple* products) that hold up to three product IDs and corresponding prices for those products. All these `P` price records are delivered in a separate file called `DATPREIS.001`, because you need fewer floppy disks if you only regularly update the prices rather than the entirety of all product details (given you already have imported them before). Sometimes you're dependent on those `P` records from another file, because the `A` record may specify a recommended selling price whereas the corresponding `P` may have discount details that tell you the purchase price.
51
+
52
+ * Only in V5 there exists a `C` record that specifies additional data for one product, such as public tender descriptions and how much work time it normally takes to physically install a product.
53
+
54
+ # About this Rubygem
55
+
56
+ What you find here is the minimal Ruby code required to parse and loop over the content of a Datanorm file and works predominantly with version 4. Version 5 support is not fully implemented.
57
+
58
+ It does not cover all features that Datanorm has, but it supports everything to get you started with the most common data attributes.
59
+
60
+ If you encounter a parsing or price calculation bug, it's likely that I didn't encounter your Datanorm file yet. I appreciate your pull request in that case.
61
+
62
+ ### Parsing technique
63
+
64
+ As explained above, there are cross-references within the Datanorm file, where one line may reference another, which is located at an arbitrary line position.
65
+
66
+ If you're lucky, the file only contains `A` and `D` records that are located closely to one another, which makes linear file parsing somewhat possible.
67
+
68
+ If you 're out of luck, there are a bunch of `T` records at the beginning of the file, then some `A` and `B` records, or the other way around.
69
+
70
+ If you have `P` records, it's really complex, because one `P` record can represent one price of one product, or two different prices for the same product, or three different prices for three entirely different products.
71
+
72
+ You, as a Ruby developer, would most likely do something like this:
73
+
74
+ ```ruby
75
+ Datanorm::Document.new(path:).each do |item|
76
+ # Here you would have *one* Object that represents
77
+ # *one* product and *all* its attributes.
78
+ end
79
+ ```
80
+
81
+ That's what we're doing. For that to work, however, we need to
82
+
83
+ * parse the entire file (may take many minutes for large files)
84
+ * remember, categorize and cache the data (on disk, because you don't want 3 GB in RAM)
85
+ * then enumerate over every product
86
+ * while doing so, gather the referenced attributes that belong to each product (sometimes referencing one part of one line, as in "P" record sets)
87
+ * wrap it all in a Ruby object and yield it as UTF-8 to you
88
+
89
+ I know there are smart ways to make this faster. But "being smart" was probably the reason that the file format grew in complexity in the first place.
90
+
91
+ I went for a parsing mechanism that works every time, with every file, at the expense of running a little bit longer than needed.
92
+
93
+ ## Usage
94
+
95
+ If you have a `DATANORM.001` and also a `DATPREIS.001`, you must concatenate those two files into one file first (their versions need to be the same). The resulting, merged file is what you provide to this Rubygem.
96
+
97
+ If you want one product at a time, without having to deal with the complexities of Datanorm, you can use this:
98
+
99
+ ```ruby
100
+ document = Datanorm::Document.new(path: 'datanorm.001')
101
+
102
+ puts document.header
103
+ puts document.version
104
+
105
+ document.each do |product, progress|
106
+ # Once pre-processing is complete, you'll start to get products here
107
+ puts product # <- can be nil in the beginning
108
+
109
+ # You can always look at the progress to see what's going on.
110
+ puts progress if progress.significant? # Throttling, so your STDOUT doesn't get spammed.
111
+ end
112
+ ```
113
+
114
+ In case you only want the raw Datanorm file one line at a time as Ruby Objects, you can use this:
115
+
116
+ ```ruby
117
+ file = Datanorm::File.new(path: 'datanorm.001')
118
+
119
+ puts file.header
120
+ puts file.version
121
+ puts file.lines_count
122
+
123
+ file.each do |record, line_number|
124
+ puts record
125
+ end
126
+ ```
127
+
128
+ **Debugging**
129
+
130
+ You can set the ENV variable `DEBUG_DATANORM=1` for verbose logging output.
131
+ You can also inspect the denormalization cache located at `/tmp/datanorm_ruby`
132
+ (it won't be automatically deleted if you set the `DEBUG_DATANORM` flag).
133
+
134
+ ## Development
135
+
136
+ Throughout the code, the following terms are used:
137
+
138
+ * `line` is one line of a Datanorm file in its raw format.
139
+ * `record` is one Ruby Object representing one of those `line`s.
140
+ * `product` is a product (article) and all its (immediate and referenced) attributes.
141
+
142
+ Run unit tests with `bin/tests`.
143
+
144
+ To get you started, you can run `bin/demo path/to/your/datanorm.001` to show its contents.
145
+
146
+ There are a few example Datanorm files in the test folder, but their characters don't always have the standard Datanorm CP850 encoding (through my tooling I often accidentally convert to UTF-8 or ASCII). Fĭx iẗ iƒ ȳou çaƞ.
147
+
148
+ ## Open Source Maintenance
149
+
150
+ This software is release under the MIT license (see LICENSE.md).
151
+
152
+ I already anticipate people sending me their various Datanorm files, thinking that I can fix their problems, but I really don't want to 😂.
153
+
154
+ Let me be clear: Nobody should use this data format. It's from the digital stone age. If you have to parse it in Ruby and need more features, I'll gladly welcome a pull request with proper test coverage.
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datanorm
4
+ # Loads and parses a datanorm file product by product.
5
+ class Document
6
+ include Datanorm::Logging
7
+ include Enumerable
8
+
9
+ attr_reader :path
10
+
11
+ def initialize(path:, timestamp: nil)
12
+ @path = path
13
+
14
+ if timestamp
15
+ # Re-use an existing workdir in case the preprocessing was already done earlier.
16
+ @timestamp = timestamp
17
+ @preprocessed = true
18
+ else
19
+ @timestamp = (Time.now.to_f * 1_000_000_000).to_i.to_s # Timestamp with nanoseconds
20
+ end
21
+ end
22
+
23
+ def header
24
+ file.header
25
+ end
26
+
27
+ def version
28
+ file.version
29
+ end
30
+
31
+ def each(&)
32
+ unless @preprocessed
33
+ ::Datanorm::Documents::Preprocess.call(file:, workdir:, &)
34
+ @preprocessed = true
35
+ end
36
+
37
+ ::Datanorm::Documents::Assemble.call(workdir:, &)
38
+ ensure
39
+ # At this point all yields have gone through and we can clean up.
40
+ workdir.rmtree unless ENV['DEBUG_DATANORM']
41
+ end
42
+
43
+ def workdir
44
+ @workdir ||= Pathname.new('/tmp/datanorm_ruby').join(@timestamp)
45
+ end
46
+
47
+ private
48
+
49
+ def file
50
+ return @file if defined?(@file)
51
+
52
+ @file = ::Datanorm::File.new(path:)
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datanorm
4
+ module Documents
5
+ # Yields every product found in the text files that the preprocessing generated.
6
+ class Assemble
7
+ include Calls
8
+ include ::Datanorm::Logging
9
+
10
+ option :workdir
11
+
12
+ def call
13
+ return unless products_file.file?
14
+
15
+ ::File.foreach(products_file) do |json|
16
+ progress.increment!
17
+ yield ::Datanorm::Documents::Assembles::Product.new(json:, workdir:), progress
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def products_file
24
+ workdir.join('A.txt')
25
+ end
26
+
27
+ def products_count
28
+ return @products_count if defined?(@products_count)
29
+
30
+ @products_count = 0
31
+ ::File.foreach(products_file) { @products_count += 1 }
32
+ @products_count
33
+ end
34
+
35
+ def progress
36
+ @progress ||= ::Datanorm::Progress.new.tap do |progress|
37
+ progress.current = 0
38
+ progress.total = products_count
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datanorm
4
+ module Documents
5
+ module Assembles
6
+ # Object wrapper for a single Price (that belongs to a Priceset).
7
+ class Price
8
+ attr_reader :as_json
9
+
10
+ def initialize(json:)
11
+ @as_json = JSON.parse(json, symbolize_names: true)
12
+ end
13
+
14
+ # -----------------
15
+ # Native Attributes
16
+ # -----------------
17
+
18
+ def wholesale?
19
+ as_json[:is_wholesale]
20
+ end
21
+
22
+ def retail?
23
+ as_json[:is_retail]
24
+ end
25
+
26
+ def no_discount?
27
+ as_json[:is_no_discount]
28
+ end
29
+
30
+ def percentage_discount?
31
+ as_json[:is_percentage_discount]
32
+ end
33
+
34
+ def discount_percentage_integer
35
+ as_json[:discount_percentage]
36
+ end
37
+
38
+ def cents
39
+ as_json[:cents].to_i
40
+ end
41
+
42
+ # ---------------------
43
+ # Calculated Attributes
44
+ # ---------------------
45
+
46
+ def price
47
+ BigDecimal(cents) / 100
48
+ end
49
+
50
+ def discount_percentage
51
+ return unless percentage_discount?
52
+
53
+ # 3700 == 37% == 0.37
54
+ BigDecimal(discount_percentage_integer) / 100 / 100
55
+ end
56
+
57
+ # What is the final price after the discount?
58
+ def price_after_discount
59
+ return price if no_discount?
60
+ return unless discount_percentage
61
+
62
+ price * (1 - discount_percentage)
63
+ end
64
+
65
+ # Helpers
66
+
67
+ def <=>(other)
68
+ precedence <=> other.precedence
69
+ end
70
+
71
+ # So we can distinguish between multiple conflicting prices.
72
+ def precedence
73
+ return 2 if no_discount?
74
+ return 1 if percentage_discount?
75
+
76
+ 0
77
+ end
78
+
79
+ def to_s
80
+ "<Price #{as_json}>"
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datanorm
4
+ module Documents
5
+ module Assembles
6
+ # Object wrapper for a single Product with all its attributes.
7
+ class Product
8
+ include ::Datanorm::Logging
9
+
10
+ def initialize(json:, workdir:)
11
+ @json = JSON.parse(json, symbolize_names: true)
12
+ @workdir = workdir
13
+
14
+ load_files!
15
+ end
16
+
17
+ # ------
18
+ # Basics
19
+ # ------
20
+
21
+ def id
22
+ json[:id]
23
+ end
24
+
25
+ def quantity
26
+ json[:quantity]
27
+ end
28
+
29
+ def quantity_unit
30
+ json[:quantity_unit]
31
+ end
32
+
33
+ def discount_group
34
+ json[:discount_group]
35
+ end
36
+
37
+ # -------
38
+ # Textual
39
+ # -------
40
+
41
+ def title
42
+ json[:title]
43
+ end
44
+
45
+ def text_id
46
+ json[:text_id]
47
+ end
48
+
49
+ def description
50
+ # In theory, the dimension is for this product only
51
+ # and the text shared by several products of the same kind.
52
+ # In practice, those two are not intended for stacking.
53
+ # Instead, we choose one or the other.
54
+ return dimension_content if dimension_content && !dimension_content.strip.empty?
55
+
56
+ text_content
57
+ end
58
+
59
+ # -----------------------
60
+ # Immediate Price details
61
+ # -----------------------
62
+
63
+ def retail_price?
64
+ json[:is_retail_price]
65
+ end
66
+
67
+ def wholesale_price?
68
+ json[:is_wholesale_price]
69
+ end
70
+
71
+ def cents
72
+ json[:cents]
73
+ end
74
+
75
+ # Convenience shortcut.
76
+ def price
77
+ BigDecimal(cents) / 100
78
+ end
79
+
80
+ # ------------------------
81
+ # Referenced Price details
82
+ # ------------------------
83
+
84
+ def prices
85
+ return @prices if defined?(@prices)
86
+
87
+ @prices = prices_content&.split("\n")&.map do |json|
88
+ ::Datanorm::Documents::Assembles::Price.new(json:)
89
+ end || []
90
+ end
91
+
92
+ # -----------------
93
+ # Referenced Extras
94
+ # -----------------
95
+
96
+ def matchcode
97
+ extra_json[:matchcode]
98
+ end
99
+
100
+ def alternative_id
101
+ extra_json[:alternative_id]
102
+ end
103
+
104
+ def ean
105
+ extra_json[:ean]
106
+ end
107
+
108
+ def category_id
109
+ extra_json[:category_id]
110
+ end
111
+
112
+ # -------
113
+ # Helpers
114
+ # -------
115
+
116
+ def to_s
117
+ "<Product #{as_json}>"
118
+ end
119
+
120
+ def as_json
121
+ # Adding referenced attributes that were cached to disk during preprocessing.
122
+ json.merge(description:, prices: prices.map(&:as_json)).merge(extra_json)
123
+ end
124
+
125
+ def to_json(...)
126
+ as_json.to_json(...)
127
+ end
128
+
129
+ private
130
+
131
+ attr_reader :json, :workdir
132
+
133
+ # The temporary cached files may be deleted quickly, so let's fetch what we need.
134
+ # effectively populates all data we need
135
+ alias load_files! as_json
136
+
137
+ def dimension_content
138
+ return @dimension_content if defined?(@dimension_content)
139
+
140
+ @dimension_content = begin
141
+ path = workdir.join('D', ::Datanorm::Helpers::Filename.call(id))
142
+ path.read if path.file?
143
+ end
144
+ end
145
+
146
+ def text_content
147
+ return unless text_id
148
+ return @text_content if defined?(@text_content)
149
+
150
+ @text_content = begin
151
+ path = workdir.join('T', ::Datanorm::Helpers::Filename.call(text_id))
152
+ path.read if path.file?
153
+ end
154
+ end
155
+
156
+ def extra_json
157
+ return @extra_json if defined?(@extra_json)
158
+
159
+ @extra_json = begin
160
+ path = workdir.join('B', ::Datanorm::Helpers::Filename.call(id))
161
+ JSON.parse(path.read, symbolize_names: true) if path.file?
162
+ end
163
+ end
164
+
165
+ def prices_content
166
+ return @prices_content if defined?(@prices_content)
167
+
168
+ @prices_content = begin
169
+ path = workdir.join('P', ::Datanorm::Helpers::Filename.call(id))
170
+ path.read if path.file?
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datanorm
4
+ module Documents
5
+ # Takes an entire Datanorm file and writes many small text files from it
6
+ # so that the content can later be iterated over in an efficient way.
7
+ class Preprocess
8
+ include Calls
9
+ include ::Datanorm::Logging
10
+
11
+ option :file
12
+ option :workdir
13
+
14
+ def call
15
+ FileUtils.mkdir_p(workdir)
16
+
17
+ file.each do |record|
18
+ ::Datanorm::Documents::Preprocesses::Process.call(workdir:, record:)
19
+
20
+ progress.increment!
21
+ yield nil, progress # No items to yield during preprocess.
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def progress
28
+ @progress ||= ::Datanorm::Progress.new.tap do |progress|
29
+ progress.title = 'Preprocessing'
30
+ progress.current = 0
31
+ progress.total = file.lines_count
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datanorm
4
+ module Documents
5
+ module Preprocesses
6
+ # Writes a Datanorm record to disk for later retrieval.
7
+ class Cache
8
+ include Calls
9
+ include ::Datanorm::Logging
10
+
11
+ option :workdir, as: :parent_workdir
12
+ option :namespace
13
+ option :id
14
+ option :target_line_number, default: -> { 1 }
15
+ option :content, as: :raw_content # Encoding::CP850
16
+
17
+ def call
18
+ do_ensure_workdir
19
+ .on_success { do_read_currently_cached_lines }
20
+ .on_success { do_amend_content }
21
+ .on_success { do_write_to_file }
22
+ end
23
+
24
+ def do_ensure_workdir
25
+ return Tron.success :workdir_exists if workdir.directory?
26
+
27
+ log { "Creating working dir `#{workdir}`" }
28
+ FileUtils.mkdir_p(workdir)
29
+ Tron.success(:workdir_created)
30
+ end
31
+
32
+ def do_read_currently_cached_lines
33
+ if filepath.exist?
34
+ @lines = filepath.readlines(chomp: true)
35
+ Tron.success(:loaded_current_file_content)
36
+ else
37
+ @lines = []
38
+ Tron.success(:nothing_cached_yet)
39
+ end
40
+ end
41
+
42
+ def do_amend_content
43
+ # Populate every line up until the wanted line.
44
+ @lines[target_line_number - 1] ||= ''
45
+
46
+ # Insert the content
47
+ @lines[target_line_number - 1] = content
48
+
49
+ Tron.success(:inserted_content)
50
+ end
51
+
52
+ def do_write_to_file
53
+ log { "Writing line(s) at position #{target_line_number} to #{filepath}" }
54
+ filepath.write @lines.join("\n")
55
+
56
+ Tron.success :wrote_to_cache
57
+ end
58
+
59
+ private
60
+
61
+ def content
62
+ raw_content.encode 'UTF-8'
63
+ end
64
+
65
+ def filepath
66
+ workdir.join(::Datanorm::Helpers::Filename.call(id))
67
+ end
68
+
69
+ def workdir
70
+ namespace_parts = Array(namespace)
71
+ parent_workdir.join(*namespace_parts)
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end