file_composer 1.0.0.pre.alpha → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +1 -1
- data/README.md +122 -5
- data/file_composer.gemspec +3 -3
- data/lib/file_composer/blueprint.rb +3 -3
- data/lib/file_composer/documents.rb +5 -1
- data/lib/file_composer/documents/result.rb +3 -1
- data/lib/file_composer/documents/zip.rb +8 -2
- data/lib/file_composer/stores/local.rb +14 -10
- data/lib/file_composer/version.rb +1 -1
- metadata +10 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ad520dd24873c117d6f7e9ab5ef900ed90b01b3292cb196ce844f06d2902dae2
|
4
|
+
data.tar.gz: fde077efef63fd9b162015a9c1d8283aac7658a7d38b53a43cc0a64d5fa6498c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bfa28568e0235ca9b78000c1ee8ebb300242bf490d57ae437360c9aecba8a4671588fe3e86938e675cd1940cb09084adea6fc80a6d5e4b895eece6f755cc24b1
|
7
|
+
data.tar.gz: fdcf7bacac07f821b2f62709551ea205680d1e59e8284e0c8431f69ef688659356a69975a5c0ce608081fb2e1438b80c0bf4362b57b71de095d694e2631eecc7
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -2,25 +2,142 @@
|
|
2
2
|
|
3
3
|
[![Gem Version](https://badge.fury.io/rb/file_composer.svg)](https://badge.fury.io/rb/file_composer) [![Build Status](https://travis-ci.org/bluemarblepayroll/file_composer.svg?branch=master)](https://travis-ci.org/bluemarblepayroll/file_composer) [![Maintainability](https://api.codeclimate.com/v1/badges/5360d687b0e93a4c7cf5/maintainability)](https://codeclimate.com/github/bluemarblepayroll/file_composer/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/5360d687b0e93a4c7cf5/test_coverage)](https://codeclimate.com/github/bluemarblepayroll/file_composer/test_coverage) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
|
4
4
|
|
5
|
-
|
5
|
+
The library can serve as a foundation for creating a composable and declarative file creation API. Out of the box, it only provides for the creation of text files and zip archives. Within zip archives you can compose N number of nested zip archives and text files. It is designed to first write to disk, then move the written files elsewhere, give then store passed in. This library is intended to be extended by registering your own document types and then using File Composer as a higher-order configuration layer.
|
6
6
|
|
7
7
|
## Installation
|
8
8
|
|
9
9
|
To install through Rubygems:
|
10
10
|
|
11
|
-
````
|
12
|
-
gem install
|
11
|
+
````bash
|
12
|
+
gem install file_composer
|
13
13
|
````
|
14
14
|
|
15
15
|
You can also add this to your Gemfile:
|
16
16
|
|
17
|
-
````
|
17
|
+
````bash
|
18
18
|
bundle add file_composer
|
19
19
|
````
|
20
20
|
|
21
21
|
## Examples
|
22
22
|
|
23
|
-
|
23
|
+
### Writing Text Files
|
24
|
+
|
25
|
+
The simplest way to start would be to write a couple text files. We declare this as:
|
26
|
+
|
27
|
+
````ruby
|
28
|
+
config = {
|
29
|
+
documents: [
|
30
|
+
{
|
31
|
+
type: :text,
|
32
|
+
filename: 'hello.txt',
|
33
|
+
data: 'hello world!'
|
34
|
+
},
|
35
|
+
{
|
36
|
+
type: :text,
|
37
|
+
filename: 'hello2.txt',
|
38
|
+
data: 'hello world again!'
|
39
|
+
}
|
40
|
+
]
|
41
|
+
}
|
42
|
+
````
|
43
|
+
|
44
|
+
And then execute it:
|
45
|
+
|
46
|
+
````ruby
|
47
|
+
blueprint = FileComposer::Blueprint.make(config)
|
48
|
+
|
49
|
+
results = blueprint.write!
|
50
|
+
````
|
51
|
+
|
52
|
+
This would generate two files within the current relative path.
|
53
|
+
|
54
|
+
### Configuring the Temporary Store
|
55
|
+
|
56
|
+
In the example above, we did not specify a temporary directory to write the file to. We can do this simply by passing in a path as the first argument to `Blueprint#write!`:
|
57
|
+
|
58
|
+
````ruby
|
59
|
+
temp_path = File.join('tmp', 'file_composer')
|
60
|
+
blueprint = FileComposer::Blueprint.make(config)
|
61
|
+
|
62
|
+
results = blueprint.write!(temp_path)
|
63
|
+
````
|
64
|
+
|
65
|
+
The two files should now be located in `tmp/file_composer` within the relative path.
|
66
|
+
|
67
|
+
### Writing to Permanent Storage
|
68
|
+
|
69
|
+
The second argument for `Blueprint#write!` can be a Ruby object instance that responds to `move!(local_filename)` and returns the permanent location. By default this library only ships with two stores:
|
70
|
+
|
71
|
+
* **FileComposer::Stores::Null**: perform no move and return the temporary file path.
|
72
|
+
* **FileComposer::Stores::Local**: perform a file move and return the new file path. The file path will also be sharded using the inputted date (defaults to the current date UTC). The sharding helps ensure a more even distribution of files so one directory does not end up with all the files.
|
73
|
+
|
74
|
+
An example using a local store would be:
|
75
|
+
|
76
|
+
````ruby
|
77
|
+
root = 'storage'
|
78
|
+
store = FileComposer::Stores::Local.new(root: root)
|
79
|
+
temp_path = File.join('tmp', 'file_composer')
|
80
|
+
blueprint = FileComposer::Blueprint.make(config)
|
81
|
+
|
82
|
+
results = blueprint.write!(temp_path, store)
|
83
|
+
````
|
84
|
+
|
85
|
+
This will now produce two files within a `storage/YYYY/MM/DD` directory in the relative path. These files were initially written within the temporary store, but then moved after completion.
|
86
|
+
|
87
|
+
### Custom Stores
|
88
|
+
|
89
|
+
A store can be any Ruby object instance that responds to `move!(local_filename)` and returns the permanent location. For example, you may choose to implement a custom one that moves files to cloud-based storage (i.e. Amazon S3).
|
90
|
+
|
91
|
+
### Writing Zip Archives
|
92
|
+
|
93
|
+
Say we wanted to also include a zip archive of some other files, we could update our configuration from above to:
|
94
|
+
|
95
|
+
````ruby
|
96
|
+
config = {
|
97
|
+
documents: [
|
98
|
+
{
|
99
|
+
type: :text,
|
100
|
+
filename: 'hello.txt',
|
101
|
+
data: 'hello world!'
|
102
|
+
},
|
103
|
+
{
|
104
|
+
type: :text,
|
105
|
+
filename: 'hello2.txt',
|
106
|
+
data: 'hello world again!'
|
107
|
+
},
|
108
|
+
{
|
109
|
+
type: :zip,
|
110
|
+
filename: 'hello3.zip',
|
111
|
+
blueprint: {
|
112
|
+
documents: [
|
113
|
+
{
|
114
|
+
type: :text,
|
115
|
+
filename: 'hello4.txt',
|
116
|
+
data: 'hello world again... again!'
|
117
|
+
}
|
118
|
+
]
|
119
|
+
}
|
120
|
+
}
|
121
|
+
]
|
122
|
+
}
|
123
|
+
````
|
124
|
+
|
125
|
+
And then execute it:
|
126
|
+
|
127
|
+
````ruby
|
128
|
+
blueprint = FileComposer::Blueprint.make(config)
|
129
|
+
|
130
|
+
results = blueprint.write!
|
131
|
+
````
|
132
|
+
|
133
|
+
This would now generate three files within the current relative path. Some notes:
|
134
|
+
|
135
|
+
* The zip archive document type allows for a fully nested hierarchy, where you can have zip archives within zip archives.
|
136
|
+
* Files within the same level need to have unique, case-insensitive filenames or else an error will be raised.
|
137
|
+
|
138
|
+
### Plugging in Custom Documents
|
139
|
+
|
140
|
+
The documents, text files and zip archives, are meant to serve as 'core' documents. Consumer applications will not find much value in these types by themselves. The class `FileComposer::Documents` is a factory that is able to have document types plugged in using the [acts_as_hashable_factory](https://github.com/bluemarblepayroll/acts_as_hashable) API.
|
24
141
|
|
25
142
|
## Contributing
|
26
143
|
|
data/file_composer.gemspec
CHANGED
@@ -5,10 +5,10 @@ require './lib/file_composer/version'
|
|
5
5
|
Gem::Specification.new do |s|
|
6
6
|
s.name = 'file_composer'
|
7
7
|
s.version = FileComposer::VERSION
|
8
|
-
s.summary = 'High-level, pluggable and
|
8
|
+
s.summary = 'High-level, pluggable, and declarative API for creating files'
|
9
9
|
|
10
10
|
s.description = <<-DESCRIPTION
|
11
|
-
This library provides a
|
11
|
+
This library provides a declarative API, called a Blueprint, that allows you to specify text files and zip files and it will create them. It is pluggable so other document types and storage mediums can be added.
|
12
12
|
DESCRIPTION
|
13
13
|
|
14
14
|
s.authors = ['Matthew Ruggio']
|
@@ -35,7 +35,7 @@ Gem::Specification.new do |s|
|
|
35
35
|
s.add_development_dependency('pry', '~>0')
|
36
36
|
s.add_development_dependency('rake', '~> 13')
|
37
37
|
s.add_development_dependency('rspec', '~> 3.8')
|
38
|
-
s.add_development_dependency('rubocop', '~>0.
|
38
|
+
s.add_development_dependency('rubocop', '~>0.90.0')
|
39
39
|
s.add_development_dependency('simplecov', '~>0.18.5')
|
40
40
|
s.add_development_dependency('simplecov-console', '~>0.7.0')
|
41
41
|
end
|
@@ -19,10 +19,10 @@ module FileComposer
|
|
19
19
|
|
20
20
|
def initialize(documents: [])
|
21
21
|
@documents = Documents.array(documents)
|
22
|
+
filenames = @documents.map { |a| a.filename.downcase }
|
23
|
+
not_unique = filenames.uniq.length != @documents.length
|
22
24
|
|
23
|
-
filenames
|
24
|
-
|
25
|
-
raise ArgumentError, "not unique: #{filenames}" if filenames.uniq.length != @documents.length
|
25
|
+
raise ArgumentError, "filenames not unique: #{filenames}" if not_unique
|
26
26
|
end
|
27
27
|
|
28
28
|
def write!(temp_root = '', store = Stores::Null.new)
|
@@ -11,7 +11,11 @@ require_relative 'documents/text'
|
|
11
11
|
require_relative 'documents/zip'
|
12
12
|
|
13
13
|
module FileComposer
|
14
|
-
# Factory for building documents.
|
14
|
+
# Factory for building documents. To register new document types:
|
15
|
+
# - Implement a subclass for FileComposer::Documents::Base or a document compliant class.
|
16
|
+
# The only real constraint is that it is hashable using the acts_as_hashable class method
|
17
|
+
# and implements #write!(temp_root = '', store = Stores::Null.new)
|
18
|
+
# - Call FileComposer::Documents#register(name, class_constant_or_name)
|
15
19
|
class Documents
|
16
20
|
acts_as_hashable_factory
|
17
21
|
|
@@ -9,7 +9,9 @@
|
|
9
9
|
|
10
10
|
module FileComposer
|
11
11
|
class Documents
|
12
|
-
#
|
12
|
+
# Returns the result of a FileComposer::Document#write! call. Each #write! call can produce
|
13
|
+
# N number of documents and each document will be represented in this instance's file_results
|
14
|
+
# attribute.
|
13
15
|
class Result
|
14
16
|
attr_reader :file_results,
|
15
17
|
:time_in_seconds
|
@@ -23,8 +23,14 @@ module FileComposer
|
|
23
23
|
|
24
24
|
def write!(temp_root = '', store = Stores::Null.new)
|
25
25
|
results = blueprint.write!(temp_root)
|
26
|
-
|
27
|
-
physical_filename =
|
26
|
+
write_time_in_seconds = results.sum(&:time_in_seconds)
|
27
|
+
physical_filename = nil
|
28
|
+
|
29
|
+
zip_time_in_seconds = Benchmark.measure do
|
30
|
+
physical_filename = zip!(temp_root, results)
|
31
|
+
end.real
|
32
|
+
|
33
|
+
total_time_in_seconds = write_time_in_seconds + zip_time_in_seconds
|
28
34
|
|
29
35
|
cleanup(results)
|
30
36
|
|
@@ -9,7 +9,17 @@
|
|
9
9
|
|
10
10
|
module FileComposer
|
11
11
|
module Stores
|
12
|
-
# File copier from local file system to the local file system
|
12
|
+
# File copier from local file system to the local file system but with a sharded path
|
13
|
+
# with YYYY/MM/DD using the passed in date. The date will default
|
14
|
+
# to the current date in UTC unless specified otherwise. The filename passed in will be
|
15
|
+
# used to determine the extension but the destination will create a new GUID to use as
|
16
|
+
# the filename. For example:
|
17
|
+
# - input: some/path/input.txt
|
18
|
+
# - output: 2020/06/25/82d042eb-9592-47f1-8019-2f06f73dc053.txt
|
19
|
+
# The reason it returns a completely random name is to ensure the file truly has a unique
|
20
|
+
# place to live, independent of what was passed in. You are free to change these assumptions
|
21
|
+
# by creating and using your own store if any of these implementation details do not fit
|
22
|
+
# your specific use case.
|
13
23
|
class Local
|
14
24
|
attr_reader :date, :root
|
15
25
|
|
@@ -23,8 +33,7 @@ module FileComposer
|
|
23
33
|
def move!(filename)
|
24
34
|
make_final_filename(filename).tap do |final_filename|
|
25
35
|
ensure_directory_exists(final_filename)
|
26
|
-
FileUtils.
|
27
|
-
FileUtils.rm(filename, force: true)
|
36
|
+
FileUtils.mv(filename, final_filename)
|
28
37
|
end
|
29
38
|
end
|
30
39
|
|
@@ -38,17 +47,12 @@ module FileComposer
|
|
38
47
|
|
39
48
|
def random_filename_parts(extension)
|
40
49
|
[
|
50
|
+
root,
|
41
51
|
date.year.to_s,
|
42
52
|
date.month.to_s,
|
43
53
|
date.day.to_s,
|
44
54
|
"#{SecureRandom.uuid}#{extension}"
|
45
|
-
].
|
46
|
-
parts.unshift(root) if root?
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
def root?
|
51
|
-
!root.empty?
|
55
|
+
].compact
|
52
56
|
end
|
53
57
|
|
54
58
|
def ensure_directory_exists(filename)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: file_composer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.0
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matthew Ruggio
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-09-
|
11
|
+
date: 2020-09-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: acts_as_hashable
|
@@ -100,14 +100,14 @@ dependencies:
|
|
100
100
|
requirements:
|
101
101
|
- - "~>"
|
102
102
|
- !ruby/object:Gem::Version
|
103
|
-
version: 0.
|
103
|
+
version: 0.90.0
|
104
104
|
type: :development
|
105
105
|
prerelease: false
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
107
107
|
requirements:
|
108
108
|
- - "~>"
|
109
109
|
- !ruby/object:Gem::Version
|
110
|
-
version: 0.
|
110
|
+
version: 0.90.0
|
111
111
|
- !ruby/object:Gem::Dependency
|
112
112
|
name: simplecov
|
113
113
|
requirement: !ruby/object:Gem::Requirement
|
@@ -136,9 +136,9 @@ dependencies:
|
|
136
136
|
- - "~>"
|
137
137
|
- !ruby/object:Gem::Version
|
138
138
|
version: 0.7.0
|
139
|
-
description: " This library provides a
|
140
|
-
|
141
|
-
|
139
|
+
description: " This library provides a declarative API, called a Blueprint, that
|
140
|
+
allows you to specify text files and zip files and it will create them. It is pluggable
|
141
|
+
so other document types and storage mediums can be added.\n"
|
142
142
|
email:
|
143
143
|
- mruggio@bluemarblepayroll.com
|
144
144
|
executables: []
|
@@ -192,12 +192,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
192
192
|
version: '2.5'
|
193
193
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
194
194
|
requirements:
|
195
|
-
- - "
|
195
|
+
- - ">="
|
196
196
|
- !ruby/object:Gem::Version
|
197
|
-
version:
|
197
|
+
version: '0'
|
198
198
|
requirements: []
|
199
199
|
rubygems_version: 3.0.3
|
200
200
|
signing_key:
|
201
201
|
specification_version: 4
|
202
|
-
summary: High-level, pluggable and
|
202
|
+
summary: High-level, pluggable, and declarative API for creating files
|
203
203
|
test_files: []
|