file_structure 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b0896ec610bd688790fad3b0f995766076ffee13148ff16ee3f9e9ae7a646b34
4
+ data.tar.gz: e3a445ce085c12d7a7971dbed9469b48247d0a130fdb91acb2b28696f84d9a62
5
+ SHA512:
6
+ metadata.gz: 48cd8e05e96096a21265e5c3e69f7bc5bb2bf11e5094984006f938dc79fd186fcbe8ac9f97d31d11a431db756f940780741924abdac37fccdc555e51bba75f6a
7
+ data.tar.gz: c12c1441fd3dc451fdbe179bec7c4b0a90f8f473990de86273b9200db757aa75f173a12b35b7c071e5ccb1642b55c58aff5554666549cdafc95ba147dde4e2ee
data/.rubocop.yml ADDED
@@ -0,0 +1,45 @@
1
+ # See list of defaults here: https://docs.rubocop.org/rubocop/index.html
2
+
3
+ require:
4
+ - rubocop-performance
5
+ - rubocop-minitest
6
+ - rubocop-rake
7
+
8
+ AllCops:
9
+ TargetRubyVersion: 2.6
10
+ NewCops: enable
11
+
12
+ # ------------------------------------------------------------------------------
13
+ # DEPARTMENT LAYOUT
14
+ # ------------------------------------------------------------------------------
15
+
16
+ Layout/FirstArrayElementIndentation:
17
+ EnforcedStyle: consistent # default: special_inside_parentheses
18
+
19
+ Layout/FirstHashElementIndentation:
20
+ EnforcedStyle: consistent # default: special_inside_parentheses
21
+
22
+ Layout/LineLength:
23
+ Max: 80 # default: 120
24
+
25
+ # ------------------------------------------------------------------------------
26
+ # DEPARTMENT METRICS
27
+ # ------------------------------------------------------------------------------
28
+
29
+ Metrics/AbcSize:
30
+ Max: 20 # default: 17
31
+
32
+ Metrics/BlockLength:
33
+ Exclude:
34
+ - file_structure.gemspec
35
+ - test/**/test_*.rb
36
+
37
+ Metrics/MethodLength:
38
+ Max: 15 # default: 10
39
+
40
+ # ------------------------------------------------------------------------------
41
+ # DEPARTMENT STYLE
42
+ # ------------------------------------------------------------------------------
43
+
44
+ Style/Documentation:
45
+ Enabled: false # default: true
data/Gemfile ADDED
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in file_structure.gemspec
6
+ gemspec
7
+
8
+ group :development do
9
+ gem 'rake', '~> 13.0'
10
+ gem 'rubocop', '~> 1.21', require: false
11
+ gem 'rubocop-minitest', '~> 0.17.2', require: false
12
+ gem 'rubocop-performance', '~> 1.13', require: false
13
+ gem 'rubocop-rake', '~> 0.6.0', require: false
14
+ gem 'yard', '~> 0.9.27', require: false
15
+ end
16
+
17
+ group :test do
18
+ gem 'minitest', '~> 5.0'
19
+ gem 'minitest-reporters', '~> 1.5'
20
+ end
21
+
22
+ group :development, :test do
23
+ gem 'debug', '~> 1.4'
24
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,73 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ file_structure (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ ansi (1.5.0)
10
+ ast (2.4.2)
11
+ builder (3.2.4)
12
+ debug (1.4.0)
13
+ irb (>= 1.3.6)
14
+ reline (>= 0.2.7)
15
+ io-console (0.5.11)
16
+ irb (1.4.1)
17
+ reline (>= 0.3.0)
18
+ minitest (5.15.0)
19
+ minitest-reporters (1.5.0)
20
+ ansi
21
+ builder
22
+ minitest (>= 5.0)
23
+ ruby-progressbar
24
+ parallel (1.21.0)
25
+ parser (3.1.1.0)
26
+ ast (~> 2.4.1)
27
+ rainbow (3.1.1)
28
+ rake (13.0.6)
29
+ regexp_parser (2.2.1)
30
+ reline (0.3.1)
31
+ io-console (~> 0.5)
32
+ rexml (3.2.5)
33
+ rubocop (1.25.1)
34
+ parallel (~> 1.10)
35
+ parser (>= 3.1.0.0)
36
+ rainbow (>= 2.2.2, < 4.0)
37
+ regexp_parser (>= 1.8, < 3.0)
38
+ rexml
39
+ rubocop-ast (>= 1.15.1, < 2.0)
40
+ ruby-progressbar (~> 1.7)
41
+ unicode-display_width (>= 1.4.0, < 3.0)
42
+ rubocop-ast (1.16.0)
43
+ parser (>= 3.1.1.0)
44
+ rubocop-minitest (0.17.2)
45
+ rubocop (>= 0.90, < 2.0)
46
+ rubocop-performance (1.13.2)
47
+ rubocop (>= 1.7.0, < 2.0)
48
+ rubocop-ast (>= 0.4.0)
49
+ rubocop-rake (0.6.0)
50
+ rubocop (~> 1.0)
51
+ ruby-progressbar (1.11.0)
52
+ unicode-display_width (2.1.0)
53
+ webrick (1.7.0)
54
+ yard (0.9.27)
55
+ webrick (~> 1.7.0)
56
+
57
+ PLATFORMS
58
+ x86_64-linux
59
+
60
+ DEPENDENCIES
61
+ debug (~> 1.4)
62
+ file_structure!
63
+ minitest (~> 5.0)
64
+ minitest-reporters (~> 1.5)
65
+ rake (~> 13.0)
66
+ rubocop (~> 1.21)
67
+ rubocop-minitest (~> 0.17.2)
68
+ rubocop-performance (~> 1.13)
69
+ rubocop-rake (~> 0.6.0)
70
+ yard (~> 0.9.27)
71
+
72
+ BUNDLED WITH
73
+ 2.3.7
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 Rémi Durieu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # file_structure
2
+
3
+ [![Test](https://github.com/durierem/file_structure/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/durierem/file_structure/actions/workflows/test.yml)
4
+ [![Lint](https://github.com/durierem/file_structure/actions/workflows/lint.yml/badge.svg?branch=main)](https://github.com/durierem/file_structure/actions/workflows/lint.yml)
5
+
6
+ Define a file structure with a `Hash` with support for files, file content,
7
+ directories and symlinks across the structure. Mount and unmount the structure
8
+ in a directory on the file system.
9
+
10
+ Useful for creating test environment for programs that manipulate
11
+ files but can be used as is for something else entirely.
12
+
13
+ ## Usage
14
+
15
+ ```ruby
16
+ # Example creating the following file hierarchy:
17
+ # /tmp/mydir
18
+ # ├── dir1
19
+ # │ ├── dir2
20
+ # │ │ └── file2
21
+ # │ ├── file3
22
+ # │ └── link_to_file2 -> /tmp/mydir/dir1/dir2/file2
23
+ # └── file1
24
+
25
+ fs = FileStructure.new([
26
+ { type: :file, name: 'file1' },
27
+ {
28
+ type: :directory,
29
+ name: 'dir1',
30
+ children: [
31
+ {
32
+ type: :directory,
33
+ name: 'dir2',
34
+ children: [
35
+ { type: :file, name: 'file2', ref: 'ref_file2', content: 'abc' }
36
+ ]
37
+ },
38
+ { type: :file, name: 'file3' },
39
+ { type: :symlink, name: 'link_to_file2', to: 'ref_file2' }
40
+ ]
41
+ }
42
+ ])
43
+
44
+ fs.mount('/tmp/mydir') # also creates the /tmp/mydir directory if it doesn't exist
45
+ fs.mounted? # => true
46
+ fs.mountpoint # => "/tmp/mydir"
47
+ fs.unmount # deletes all files in /tmp/mydir
48
+
49
+ JSON.dump(fs.structure) # (bonus) easily serializable :D
50
+ ```
51
+
52
+ ## License
53
+
54
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rake/testtask'
5
+ require 'rubocop/rake_task'
6
+
7
+ Rake::TestTask.new(:test) do |t|
8
+ t.libs << 'test'
9
+ t.libs << 'lib'
10
+ t.test_files = FileList['test/**/test_*.rb']
11
+ end
12
+
13
+ RuboCop::RakeTask.new
14
+
15
+ desc 'Generate documentation with Yard'
16
+ task :doc do
17
+ sh 'yard doc --no-private'
18
+ end
19
+
20
+ task default: %i[test rubocop doc]
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/file_structure/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'file_structure'
7
+ spec.version = FileStructure::VERSION
8
+ spec.authors = ['Rémi Durieu']
9
+ spec.email = ['mail@remidurieu.dev']
10
+
11
+ spec.summary = <<-TEXT
12
+ Manage file structures (ie: files and directories) on the file system.
13
+ TEXT
14
+ spec.description = <<-TEXT
15
+ Describe a file structure with a Ruby Hash. Supports files, file content,
16
+ directories and symlinks across the structure.
17
+
18
+ Mount and unmount the desired structured in a directory on the file system.
19
+
20
+ Useful for creating test environment for programs that manipulate
21
+ files but can be used as is for something else entirely.
22
+ TEXT
23
+ spec.homepage = 'https://github.com/durierem/file_structure'
24
+ spec.license = 'MIT'
25
+ spec.required_ruby_version = '>= 2.6.0'
26
+
27
+ spec.metadata['homepage_uri'] = spec.homepage
28
+ spec.metadata['source_code_uri'] = spec.homepage
29
+
30
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
31
+ `git ls-files -z`.split("\x0").reject do |f|
32
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|features)/|\.(?:git))})
33
+ end
34
+ end
35
+ spec.bindir = 'exe'
36
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
37
+ spec.require_paths = ['lib']
38
+
39
+ spec.metadata['rubygems_mfa_required'] = 'true'
40
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ class FileStructure
4
+ module Contract
5
+ module_function
6
+
7
+ class AssertionError < ArgumentError; end
8
+
9
+ # @param expression [Boolean] The expression to evaluate
10
+ # @param message [String] The message to attach to the raised error
11
+ #
12
+ # @raise [AssertionError] if the expression evaluates to false
13
+ #
14
+ # @api private
15
+ def assert(expression, message = '')
16
+ raise AssertionError, message unless expression
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ class FileStructure
4
+ # @private
5
+ class Validator
6
+ def initialize(structure)
7
+ @structure = structure
8
+ end
9
+
10
+ def valid?
11
+ valid_structure?(@structure)
12
+ end
13
+
14
+ private
15
+
16
+ def valid_structure?(structure)
17
+ return false unless structure.is_a?(Array)
18
+
19
+ structure.all? do |elem|
20
+ return false unless elem.is_a?(Hash)
21
+
22
+ case elem[:type]
23
+ when :file
24
+ valid_file?(elem)
25
+ when :symlink
26
+ valid_symlink?(elem)
27
+ when :directory
28
+ valid_directory?(elem)
29
+ else
30
+ false
31
+ end
32
+ end
33
+ end
34
+
35
+ def valid_file?(element)
36
+ attributes = %i[type name ref content]
37
+ element.key?(:name) && element.each_key.all? do |key|
38
+ attributes.include?(key)
39
+ end
40
+ end
41
+
42
+ def valid_symlink?(element)
43
+ attributes = %i[type name to]
44
+ element.key?(:name) && element.key?(:to) && element.each_key.all? do |key|
45
+ attributes.include?(key)
46
+ end
47
+ end
48
+
49
+ def valid_directory?(element)
50
+ attributes = %i[type name ref children]
51
+ element.key?(:name) &&
52
+ element.key?(:children) &&
53
+ element.each_key { |key| attributes.include?(key) } &&
54
+ valid_structure?(element[:children])
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class FileStructure
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require_relative 'file_structure/contract'
5
+ require_relative 'file_structure/validator'
6
+ require_relative 'file_structure/version'
7
+
8
+ # Manage files, directories and links described with a file structure
9
+ # definition as a Ruby Hash. A valid file structure definition is an Array
10
+ # of Hashes with the following possible file definitions:
11
+ #
12
+ # rubocop:disable Layout/LineLength
13
+ # @example
14
+ # { name: 'myfile', type: :file, content: 'abc', ref: 'fileref'} # :content and :ref are optional
15
+ # { name: 'mydir' type: :directory, children: [<another valid file structure definition>], ref: 'dirref' } # :ref is optional
16
+ # { name: 'mylink', type: :link, to: 'fileref' } # link to 'myfile' refereced earlier by 'fileref'
17
+ # rubocop:enable Layout/LineLength
18
+ class FileStructure
19
+ attr_reader :structure, :mountpoint
20
+
21
+ # @param structure [Array<Hash>] a valid file structure definition.
22
+ # @raise [AssertionError] if the file structure is invalid
23
+ def initialize(structure)
24
+ Contract.assert(valid_file_structure?(structure), 'invalid file structure')
25
+
26
+ @structure = structure
27
+ @mountpoint = nil
28
+ end
29
+
30
+ # Effectively creates files and directories in the specified directory.
31
+ #
32
+ # @param dirname [String] the target directory
33
+ # @raise [AssertionError] if the FileStructure is already mounted
34
+ # @return void
35
+ # @see unmount
36
+ def mount(dirname)
37
+ Contract.assert(!mounted?, 'file structure is already mounted')
38
+
39
+ mountpoint = File.absolute_path(dirname)
40
+ FileUtils.mkdir_p(mountpoint) unless Dir.exist?(mountpoint)
41
+ begin
42
+ create_file_structure(mountpoint, @structure)
43
+ rescue StandardErrror => e
44
+ FileUtils.rm_r(Dir.glob("#{mountpoint}/*")) # clear residuals
45
+ raise e
46
+ end
47
+ @mountpoint = mountpoint
48
+ end
49
+
50
+ # Remove all files from the mountpoint.
51
+ #
52
+ # @return void
53
+ # @see mount
54
+ def unmount
55
+ Contract.assert(mounted?, 'file structure is not mounted')
56
+
57
+ FileUtils.rm_r(Dir.glob("#{@mountpoint}/*"))
58
+ @mountpoint = nil
59
+ end
60
+
61
+ def mounted?
62
+ !!@mountpoint
63
+ end
64
+
65
+ # Get the full path for a file in the mounted file structure.
66
+ #
67
+ # @example
68
+ # path_for(:foo, :bar, :file)
69
+ # # => "/path/to/mountpoint/foo/bar/file"
70
+ #
71
+ # @param args [Symbol, String Array<Symbol, String>] the recursive names to
72
+ # the desired file or directory
73
+ # @return [String] the full path to the specified file/directory
74
+ # @raise [AssertionError] if the file structure is not mounted
75
+ def path_for(*args)
76
+ Contract.assert(mounted?, 'file structure is not mounted')
77
+
78
+ finder = [*args].flatten.map(&:to_sym)
79
+ build_path(finder, @structure)
80
+ end
81
+
82
+ private
83
+
84
+ def valid_file_structure?(structure)
85
+ FileStructure::Validator.new(structure).valid?
86
+ end
87
+
88
+ # @param finder [Array] such as :foo, :bar
89
+ # @param structure [Array] file structure definition
90
+ # @param path [String] starting path (recursive accumulator)
91
+ def build_path(finder, structure, path = @mountpoint)
92
+ return path if finder.empty? || structure.nil?
93
+
94
+ base = structure.find { |item| item[:name].to_s == finder.first.to_s }
95
+ return nil if base.nil?
96
+
97
+ build_path(
98
+ finder[1..],
99
+ base[:children],
100
+ File.join(path, base[:name])
101
+ )
102
+ end
103
+
104
+ # @param dirname [String] root directory
105
+ # @param structure [Array] file structure definition
106
+ # @param symlinks [Hash] current symlinks refs (recursive accumulator)
107
+ def create_file_structure(dirname, structure, symlinks = {})
108
+ structure.each do |element|
109
+ path = File.join(File.absolute_path(dirname), element[:name])
110
+ case element[:type]
111
+ when :file
112
+ File.write(path, element[:content])
113
+ symlinks.merge!(element[:ref] => path) if element[:ref]
114
+ when :symlink
115
+ FileUtils.symlink(symlinks[element[:to]], path)
116
+ when :directory
117
+ FileUtils.mkdir(path)
118
+ create_file_structure(path, element[:children], symlinks)
119
+ end
120
+ end
121
+ end
122
+ end
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: file_structure
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Rémi Durieu
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2022-02-26 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: |2
14
+ Describe a file structure with a Ruby Hash. Supports files, file content,
15
+ directories and symlinks across the structure.
16
+
17
+ Mount and unmount the desired structured in a directory on the file system.
18
+
19
+ Useful for creating test environment for programs that manipulate
20
+ files but can be used as is for something else entirely.
21
+ email:
22
+ - mail@remidurieu.dev
23
+ executables: []
24
+ extensions: []
25
+ extra_rdoc_files: []
26
+ files:
27
+ - ".rubocop.yml"
28
+ - Gemfile
29
+ - Gemfile.lock
30
+ - LICENSE.txt
31
+ - README.md
32
+ - Rakefile
33
+ - file_structure.gemspec
34
+ - lib/file_structure.rb
35
+ - lib/file_structure/contract.rb
36
+ - lib/file_structure/validator.rb
37
+ - lib/file_structure/version.rb
38
+ homepage: https://github.com/durierem/file_structure
39
+ licenses:
40
+ - MIT
41
+ metadata:
42
+ homepage_uri: https://github.com/durierem/file_structure
43
+ source_code_uri: https://github.com/durierem/file_structure
44
+ rubygems_mfa_required: 'true'
45
+ post_install_message:
46
+ rdoc_options: []
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 2.6.0
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ requirements: []
60
+ rubygems_version: 3.3.7
61
+ signing_key:
62
+ specification_version: 4
63
+ summary: 'Manage file structures (ie: files and directories) on the file system.'
64
+ test_files: []