and_feathers 0.0.1 → 1.0.0.pre

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1223a216ba02082494665468136a5511c39a9f1a
4
- data.tar.gz: ab24c8bbac7301824b242bbb875c7ff88b3b60f4
3
+ metadata.gz: c8677ebc7f56d9f906f50385f52023ac81ba58bd
4
+ data.tar.gz: c480d1890de2bc3d78f2d7da5daf317300113be0
5
5
  SHA512:
6
- metadata.gz: ce15a3c45c91982cad44943259acd6575b49f28e639bfec316a307ab973434d61b50088604898f6ad0b618b42745c54cd7ac373185d2cf4b4c0bc27d774e883c
7
- data.tar.gz: 3db4aa09d56d1d9829909d89bc5e246f2f38eeb112f4ed62643160319ac83375dda49a395ba3d7ee23563a2fffad723098837f6fb778d46a23396782c33d1f5a
6
+ metadata.gz: f9fb0a23273a125b5a519d11c3f8a5a00cde68e7a33afbf0c5159c8d2b10954b159c42fc9de5212f5a8d4ff97d0a1fec8d463910f1960f9fd29503465b6614b3
7
+ data.tar.gz: 9e541a6708157de758811e543cbc6bef2cdd25dea3dad6aac1c0243f0f288b62834eb702d1672633ef7788a53718826da671c09753080422a9dc803675fd4a43
data/.gitignore CHANGED
@@ -15,3 +15,4 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ .ruby-version
data/.rspec CHANGED
@@ -1,4 +1,4 @@
1
1
  -I spec
2
2
  -I lib
3
3
  --color
4
- --order random
4
+ -r byebug
data/Gemfile CHANGED
@@ -1,4 +1,6 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
+ gem 'byebug'
4
+
3
5
  # Specify your gem's dependencies in and_feathers.gemspec
4
6
  gemspec
data/README.md CHANGED
@@ -1,14 +1,10 @@
1
1
  # AndFeathers
2
2
 
3
- Declaratively build in-memory gzipped tarballs.
3
+ Declaratively and iteratively build in-memory archive structures.
4
4
 
5
5
  ## Installation
6
6
 
7
- Either run:
8
-
9
- $ gem install and_feathers
10
-
11
- Or, if you're using Bundler, add this line to your application's Gemfile:
7
+ Add this line to your application's Gemfile:
12
8
 
13
9
  gem 'and_feathers'
14
10
 
@@ -16,15 +12,28 @@ And then execute:
16
12
 
17
13
  $ bundle
18
14
 
15
+ Or install it yourself as:
16
+
17
+ $ gem install and_feathers
18
+
19
19
  ## Usage
20
20
 
21
- Suppose you want to create the equivalent of a Chef cookbook artifact created using knife:
21
+ The examples below focus on specifying an archive's structure using `and_feathers`. See:
22
+
23
+ * [`and_feathers-gzipped_tarball`](https://github.com/bcobb/and_feathers-gzipped_tarball) for notes on writing a `.tgz` file to disk
24
+ * [`and_feathers-zip`](https://github.com/bcobb/and_feathers-zip) for notes on writing a `.zip` file to disk
25
+
26
+ Once you're "inside" `and_feathers`, either because you've called `AndFeathers.build` or `AndFeathers.from_path`, the two main methods you'll call on block parameters are `file` and `dir`. These, as you might suspect, create file and directory entries, respectively.
27
+
28
+ The examples below show how you might use these two methods to build up directory structures.
29
+
30
+ ### Specify each directory and file individually
22
31
 
23
32
  ```ruby
24
33
  require 'and_feathers'
25
34
  require 'json'
26
35
 
27
- tarball = AndFeathers.build('redis') do |redis|
36
+ archive = AndFeathers.build('redis') do |redis|
28
37
  redis.file('README.md') { "README content" }
29
38
  redis.file('metadata.json') { JSON.dump({}) }
30
39
  redis.file('metadata.rb') { "# metadata.rb content" }
@@ -32,13 +41,50 @@ tarball = AndFeathers.build('redis') do |redis|
32
41
  attributes.file('default.rb') { '# default.rb content' }
33
42
  end
34
43
  redis.dir('recipes') do |recipes|
35
- attributes.file('default.rb') { '# default.rb content' }
44
+ recipes.file('default.rb') { '# default.rb content' }
36
45
  end
37
46
  redis.dir('templates') do |templates|
38
47
  templates.dir('default')
39
48
  end
40
49
  end
50
+ ```
51
+
52
+ ### Specify directories and files by their paths
41
53
 
42
- tarball.to_io # a gzipped, tarball StringIO
54
+ ```ruby
55
+ require 'and_feathers'
56
+
57
+ archive = AndFeathers.build('rails_app') do |app|
58
+ app.file('README.md') { "README content" }
59
+ app.file('config/routes.rb') do
60
+ "root to: 'public#home'"
61
+ end
62
+ app.dir('app/controllers') do |controllers|
63
+ controllers.file('application_controller.rb') do
64
+ "class ApplicationController < ActionController:Base\nend"
65
+ end
66
+ controllers.file('public_controller.rb') do
67
+ "class PublicController < ActionController:Base\nend"
68
+ end
69
+ end
70
+ app.file('app/views/public/home.html.erb')
71
+ end
43
72
  ```
44
73
 
74
+ ### Load an existing directory as an Archive
75
+
76
+ In the example below, we load the fixture directory at [`spec/fixtures/archiveme`](/spec/fixtures/archiveme), and then use `and_feathers` to perform surgery on the in-memory archive. In particular, we add a `test` directory and file to its archive, and update its `lib` directory a couple of times.
77
+
78
+ ```ruby
79
+ require 'and_feathers'
80
+
81
+ archive = AndFeathers.from_path('spec/fixtures/archiveme')
82
+ archive.file('test/basic_test.rb') { '# TODO: tests' }
83
+ archive.file('lib/archiveme/version.rb') do
84
+ "module Archiveme\n VERSION = '1.0.0'\nend"
85
+ end
86
+ archive.file('lib/archiveme.rb') do
87
+ # The Archiveme fixture is a class, but we'll change it to a module
88
+ "module Archiveme\nend"
89
+ end
90
+ ```
data/and_feathers.gemspec CHANGED
@@ -8,7 +8,8 @@ Gem::Specification.new do |spec|
8
8
  spec.version = AndFeathers::VERSION
9
9
  spec.authors = ["Brian Cobb"]
10
10
  spec.email = ["bcobb@uwalumni.com"]
11
- spec.summary = %q{Declaratively build GZipped Tarballs in memory}
11
+ spec.description = %q{Declaratively and iteratively build archive structures which easily serialize to on-disk formats such as zip and tgz.}
12
+ spec.summary = %q{In-memory archive structures}
12
13
  spec.homepage = "http://github.com/bcobb/and_feathers"
13
14
  spec.license = "MIT"
14
15
 
@@ -0,0 +1,99 @@
1
+ require 'and_feathers/file'
2
+ require 'and_feathers/directory'
3
+ require 'and_feathers/sugar'
4
+
5
+ module AndFeathers
6
+ #
7
+ # The parent class of a given directory tree. It knows whether or not the
8
+ # archive's contents should be extracted into its own directory or into its
9
+ # containing directory.
10
+ #
11
+ # An +Archive+ exposes the same sugary interface exposed by a +Directory+,
12
+ # but it is implemented so that adding files and directories directly to the
13
+ # +Archive+ is not destructive. In fact, whenever a file or directory is
14
+ # added to an +Archive+, the +Archive+ creates a new top-level directory, and
15
+ # delegates the change to it. That way, when it's time to enumerate the
16
+ # entries in the +Archive+, we can take the union of all of these top-level
17
+ # directories and enumerate _its_ entries. Thus, the only possibility for
18
+ # loss is if two changes modify the same file, in which case we take the
19
+ # latest file to be the authoritative file.
20
+ #
21
+ class Archive
22
+ include Sugar
23
+ include Enumerable
24
+
25
+ #
26
+ # Creates a new +Archive+
27
+ #
28
+ # @param extract_to [String] the path under which the +Archive+ should be
29
+ # extracted
30
+ # @param extraction_mode [Fixnum] the mode of the +Archive+'s base directory
31
+ #
32
+ def initialize(extract_to = '.', extraction_mode = 16877)
33
+ @initial_version = Directory.new(extract_to, extraction_mode)
34
+ @versions = [@initial_version]
35
+ @extract_to = extract_to
36
+ @extraction_mode = extraction_mode
37
+ end
38
+
39
+ #
40
+ # Adds a +Directory+ to the top level of the +Archive+
41
+ #
42
+ # @param directory [Directory]
43
+ #
44
+ def add_directory(directory)
45
+ @versions << Directory.new(@extract_to, @extraction_mode).tap do |parent|
46
+ parent.add_directory(directory)
47
+ end
48
+ end
49
+
50
+ #
51
+ # Adds a +File+ to the top level of the +Archive+
52
+ #
53
+ # @param file [File]
54
+ #
55
+ def add_file(file)
56
+ @initial_version.file(file.name, file.mode, &file.content)
57
+ end
58
+
59
+ #
60
+ # Iterates depth-first through the +Archive+'s entries
61
+ #
62
+ # @yieldparam entry [Directory, File]
63
+ #
64
+ def each(&block)
65
+ @versions.reduce(&:|).each(&block)
66
+ end
67
+
68
+ #
69
+ # Returns this +Archive+ as a package of the given +package_type+
70
+ #
71
+ # @example
72
+ # require 'and_feathers/gzipped_tarball'
73
+ #
74
+ # format = AndFeathers::GzippedTarball
75
+ # AndFeathers::Archive.new('test', 16877).to_io(format)
76
+ #
77
+ # @see https://github.com/bcobb/and_feathers-gzipped_tarball
78
+ # @see https://github.com/bcobb/and_feathers-zip
79
+ #
80
+ # @param package_type [.open,#add_file,#add_directory]
81
+ #
82
+ # @return [StringIO]
83
+ #
84
+ def to_io(package_type)
85
+ package_type.open do |package|
86
+ package.add_directory(@initial_version)
87
+
88
+ each do |child|
89
+ case child
90
+ when File
91
+ package.add_file(child)
92
+ when Directory
93
+ package.add_directory(child)
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,155 @@
1
+ require 'and_feathers/sugar'
2
+
3
+ module AndFeathers
4
+ #
5
+ # Represents a Directory
6
+ #
7
+ class Directory
8
+ include Sugar
9
+ include Enumerable
10
+
11
+ attr_reader :name, :mode
12
+ attr_writer :parent
13
+
14
+ #
15
+ # @!attribute [r] name
16
+ # @return [String] the directory name
17
+ #
18
+ # @!attribute [r] mode
19
+ # @return [Fixnum] the directory mode
20
+ #
21
+ # @!attribute [rw] parent
22
+ # @return [Directory] the directory's parent
23
+ #
24
+
25
+ #
26
+ # Creates a new +Directory+
27
+ #
28
+ # @param name [String] the directory name
29
+ # @param mode [Fixnum] the directory mode
30
+ #
31
+ def initialize(name = '.', mode = 16877)
32
+ @name = name
33
+ @mode = mode
34
+ @parent = nil
35
+ @files = {}
36
+ @directories = {}
37
+ end
38
+
39
+ #
40
+ # This +Directory+'s path
41
+ #
42
+ # @return [String]
43
+ #
44
+ def path
45
+ if @parent
46
+ ::File.join(@parent.path, name)
47
+ else
48
+ if name != '.'
49
+ ::File.join('.', name)
50
+ else
51
+ name
52
+ end
53
+ end
54
+ end
55
+
56
+ #
57
+ # Determines this +Directory+'s path relative to the given path.
58
+ #
59
+ # @return [String]
60
+ #
61
+ def path_from(relative_path)
62
+ path.sub(/^#{Regexp.escape(relative_path)}\/?/, '')
63
+ end
64
+
65
+ #
66
+ # Computes the union of this +Directory+ with another +Directory+. If the
67
+ # two directories have a file path in common, the file in the +other+
68
+ # +Directory+ takes precedence. If the two directories have a sub-directory
69
+ # path in common, the union's sub-directory path will be the union of those
70
+ # two sub-directories.
71
+ #
72
+ # @raise [ArgumentError] if the +other+ parameter is not a +Directory+
73
+ #
74
+ # @param other [Directory]
75
+ #
76
+ # @return [Directory]
77
+ #
78
+ def |(other)
79
+ if !other.is_a?(Directory)
80
+ raise ArgumentError, "#{other} is not a Directory"
81
+ end
82
+
83
+ self.dup.tap do |directory|
84
+ other.files.each do |file|
85
+ directory.add_file(file.dup)
86
+ end
87
+
88
+ other.directories.each do |new_directory|
89
+ existing_directory = @directories[new_directory.name]
90
+
91
+ if existing_directory.nil?
92
+ directory.add_directory(new_directory.dup)
93
+ else
94
+ directory.add_directory(new_directory.dup | existing_directory.dup)
95
+ end
96
+ end
97
+ end
98
+ end
99
+
100
+ #
101
+ # The +File+ entries which exist in this +Directory+
102
+ #
103
+ # @return [Array<File>]
104
+ #
105
+ def files
106
+ @files.values
107
+ end
108
+
109
+ #
110
+ # The +Directory+ entries which exist in this +Directory+
111
+ #
112
+ # @return [Array<Directory>]
113
+ #
114
+ def directories
115
+ @directories.values
116
+ end
117
+
118
+ #
119
+ # Iterates through this +Directory+'s children depth-first
120
+ #
121
+ # @yieldparam child [File, Directory]
122
+ #
123
+ def each(&block)
124
+ files.each(&block)
125
+
126
+ directories.each do |subdirectory|
127
+ block.call(subdirectory)
128
+
129
+ subdirectory.each(&block)
130
+ end
131
+ end
132
+
133
+ #
134
+ # Sets the given +directory+'s parent to this +Directory+, and adds it as a
135
+ # child.
136
+ #
137
+ # @param directory [Directory]
138
+ #
139
+ def add_directory(directory)
140
+ @directories[directory.name] = directory
141
+ directory.parent = self
142
+ end
143
+
144
+ #
145
+ # Sets the given +file+'s parent to this +Directory+, and adds it as a
146
+ # child.
147
+ #
148
+ # @param file [File]
149
+ #
150
+ def add_file(file)
151
+ @files[file.name] = file
152
+ file.parent = self
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,66 @@
1
+ module AndFeathers
2
+ #
3
+ # Represents a File inside the archive
4
+ #
5
+ class File
6
+ attr_reader :name, :mode, :content
7
+ attr_writer :parent
8
+
9
+ #
10
+ # @!attribute [r] name
11
+ # @return [String] the file's name
12
+ #
13
+ # @!attribute [r] mode
14
+ # @return [Fixnum] the file's mode
15
+ #
16
+ # @!attribute [r] content
17
+ # @return [Fixnum] a block which returns the file's content
18
+ #
19
+ # @!attribute [rw] parent
20
+ # @return [Directory] the file's parent
21
+ #
22
+
23
+ #
24
+ # Creates a new +File+
25
+ #
26
+ # @param name [String] the file name
27
+ # @param mode [Fixnum] the file mode
28
+ # @param content [Proc] a block which returns the file contents
29
+ #
30
+ def initialize(name, mode, content)
31
+ @name = name
32
+ @mode = mode
33
+ @content = content
34
+ @parent = nil
35
+ end
36
+
37
+ #
38
+ # This +File+'s path
39
+ #
40
+ # @return [String]
41
+ #
42
+ def path
43
+ if @parent
44
+ ::File.join(@parent.path, name)
45
+ else
46
+ ::File.join('.', name)
47
+ end
48
+ end
49
+
50
+ #
51
+ # Determines this +File+'s path relative to the given path.
52
+ #
53
+ # @return [String]
54
+ #
55
+ def path_from(relative_path)
56
+ path.sub(/^#{Regexp.escape(relative_path)}\/?/, '')
57
+ end
58
+
59
+ #
60
+ # This +File+'s contents
61
+ #
62
+ def read
63
+ @content.call
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,105 @@
1
+ module AndFeathers
2
+ #
3
+ # A module which provides convenience methods for defining a directory/file
4
+ # structure
5
+ #
6
+ module Sugar
7
+ #
8
+ # Add a +Directory+ named +name+ to this entity's list of children. The
9
+ # +name+ may simply be the name of the directory, or may be a path to the
10
+ # directory.
11
+ #
12
+ # In the case of the latter, +dir+ will create the +Directory+ tree
13
+ # specified by the path. The block parameter yielded in this case will be
14
+ # the innermost directory.
15
+ #
16
+ # @example
17
+ # archive = Directory.new
18
+ # archive.dir('app') do |app|
19
+ # app.name == 'app'
20
+ # app.path == './app'
21
+ # end
22
+ #
23
+ # @example
24
+ # archive.dir('app/controllers/concerns') do |concerns|
25
+ # concerns.name == 'concerns'
26
+ # concerns.path == './app/controllers/concerns'
27
+ # end
28
+ #
29
+ # @param name [String] the directory name
30
+ # @param mode [Fixnum] the directory mode
31
+ #
32
+ # @yieldparam directory [AndFeathers::Directory] the newly-created
33
+ # +Directory+
34
+ #
35
+ def dir(name, mode = 16877, &block)
36
+ name_parts = name.split(::File::SEPARATOR)
37
+
38
+ innermost_child_name = name_parts.pop
39
+
40
+ if name_parts.empty?
41
+ Directory.new(name, mode).tap do |directory|
42
+ add_directory(directory)
43
+
44
+ block.call(directory) if block
45
+ end
46
+ else
47
+ innermost_parent = name_parts.reduce(self) do |parent, child_name|
48
+ parent.dir(child_name)
49
+ end
50
+
51
+ innermost_parent.dir(innermost_child_name, &block)
52
+ end
53
+ end
54
+
55
+ #
56
+ # The default file content block, which returns an empty string
57
+ #
58
+ NO_CONTENT = Proc.new { "" }
59
+
60
+ #
61
+ # Add a +File+ named +name+ to this entity's list of children. The +name+
62
+ # may simply be the name of the file or may be a path to the file.
63
+ #
64
+ # In the case of the latter, +file+ will create the +Directory+ tree
65
+ # which contains the +File+ specified by the path.
66
+ #
67
+ # Either way, the +File+'s contents will be set to the result of the
68
+ # given block, or to a blank string if no block is given
69
+ #
70
+ # @example
71
+ # archive = Directory.new
72
+ # archive.file('README') do
73
+ # "Cool"
74
+ # end
75
+ #
76
+ # @example
77
+ # archive = Directory.new
78
+ # archive.file('app/models/user.rb') do
79
+ # "class User < ActiveRecord::Base\nend"
80
+ # end
81
+ #
82
+ # @param name [String] the file name
83
+ # @param mode [Fixnum] the file mode
84
+ #
85
+ # @yieldreturn [String] the file contents
86
+ #
87
+ def file(name, mode = 33188, &content)
88
+ content ||= NO_CONTENT
89
+
90
+ name_parts = name.split(::File::SEPARATOR)
91
+
92
+ file_name = name_parts.pop
93
+
94
+ if name_parts.empty?
95
+ File.new(name, mode, content).tap do |file|
96
+ add_file(file)
97
+ end
98
+ else
99
+ dir(name_parts.join(::File::SEPARATOR)) do |parent|
100
+ parent.file(file_name, mode, &content)
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -1,3 +1,8 @@
1
1
  module AndFeathers
2
- VERSION = "0.0.1"
2
+ #
3
+ # AndFeathers strives to adhere to semantic versioning. Version 1.0.0
4
+ # introduces the change to a configurable output format, which breaks the
5
+ # interface of the initial release.
6
+ #
7
+ VERSION = "1.0.0.pre"
3
8
  end
data/lib/and_feathers.rb CHANGED
@@ -1,6 +1,4 @@
1
- require 'and_feathers/tarball/file'
2
- require 'and_feathers/tarball/directory'
3
- require 'and_feathers/tarball'
1
+ require 'and_feathers/archive'
4
2
  require 'and_feathers/version'
5
3
 
6
4
  #
@@ -8,29 +6,70 @@ require 'and_feathers/version'
8
6
  #
9
7
  module AndFeathers
10
8
  #
11
- # Builds a new +Tarball+. If +base+ is not given, the tarball's contents
9
+ # Builds a new archive. If +base+ is not given, the archives's contents
12
10
  # would be extracted to intermingle with whichever directory contains the
13
- # tarball. If +base+ is given, the tarball's contents will live inside a
14
- # directory with that name. This is just a convenient way to have a +dir+
15
- # call wrap the tarball's contents
11
+ # archive. If +base+ is given, the archive's contents will live inside a
12
+ # directory with that name.
16
13
  #
17
- # @param base [String] name of the base directory containing the tarball's
14
+ # @param extract_to [String] name of the base directory containing the archive's
18
15
  # contents
19
- # @param base_mode [Fixnum] the mode of the base directory
16
+ # @param extraction_mode [Fixnum] the mode of the base directory
20
17
  #
21
- # @yieldparam tarball [Tarball]
18
+ # @yieldparam archive [Archive]
22
19
  #
23
- def self.build(base = nil, base_mode = 16877, &block)
24
- if base && base_mode
25
- Tarball.new.tap do |tarball|
26
- tarball.dir(base, base_mode) do |dir|
27
- block.call(dir)
28
- end
20
+ def self.build(extract_to = nil, extraction_mode = 16877, &block)
21
+ extract_to ||= '.'
22
+
23
+ Archive.new(extract_to, extraction_mode).tap do |archive|
24
+ block.call(archive)
25
+ end
26
+ end
27
+
28
+ #
29
+ # Builds a new archive from the directory at the given +path+. The
30
+ # innermost directory is taken to be the parent folder of the archive's
31
+ # contents.
32
+ #
33
+ # @param path [String] path to the directory to archive
34
+ #
35
+ # @yieldparam archive [Archive] the loaded archive
36
+ #
37
+ def self.from_path(path, &block)
38
+ if !::File.exists?(path)
39
+ raise ArgumentError, "#{path} does not exist"
40
+ end
41
+
42
+ directories, files = ::Dir[::File.join(path, '**/*')].partition do |path|
43
+ ::File.directory?(path)
44
+ end
45
+
46
+ full_path = ::File.expand_path(path)
47
+ extract_to = full_path.split(::File::SEPARATOR).last
48
+ extraction_mode = ::File.stat(full_path).mode
49
+
50
+ Archive.new(extract_to, extraction_mode).tap do |archive|
51
+ directories.map do |directory|
52
+ [
53
+ directory.sub(/^#{Regexp.escape(path)}\/?/, ''),
54
+ ::File.stat(directory).mode
55
+ ]
56
+ end.each do |directory, mode|
57
+ archive.dir(directory, mode)
29
58
  end
30
- else
31
- Tarball.new.tap do |tarball|
32
- block.call(tarball)
59
+
60
+ files.each do |file|
61
+ mode = ::File.stat(file).mode
62
+
63
+ ::File.open(file, 'rb') do |io|
64
+ content = io.read
65
+
66
+ archive.file(file.sub(/^#{Regexp.escape(path)}\/?/, ''), mode) do
67
+ content
68
+ end
69
+ end
33
70
  end
71
+
72
+ block.call(archive) if block
34
73
  end
35
74
  end
36
75
  end