file_structure 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []