and_feathers 0.0.1 → 1.0.0.pre

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