binder_core 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.
Files changed (58) hide show
  1. data/.gitignore +13 -0
  2. data/.rspec +2 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE +19 -0
  5. data/README.markdown +86 -0
  6. data/Rakefile +1 -0
  7. data/binder_core.gemspec +24 -0
  8. data/lib/binder_core/asset.rb +45 -0
  9. data/lib/binder_core/binder.rb +26 -0
  10. data/lib/binder_core/compiler.rb +97 -0
  11. data/lib/binder_core/config.rb +98 -0
  12. data/lib/binder_core/console.rb +63 -0
  13. data/lib/binder_core/debug/stack.rb +86 -0
  14. data/lib/binder_core/defaults.rb +42 -0
  15. data/lib/binder_core/definition.rb +12 -0
  16. data/lib/binder_core/file_context.rb +46 -0
  17. data/lib/binder_core/file_ref.rb +38 -0
  18. data/lib/binder_core/folder_context.rb +55 -0
  19. data/lib/binder_core/parser.rb +82 -0
  20. data/lib/binder_core/parsers/asset_parser.rb +7 -0
  21. data/lib/binder_core/parsers/folder_parser.rb +7 -0
  22. data/lib/binder_core/parsers/null_parser.rb +6 -0
  23. data/lib/binder_core/parsers/text_parser.rb +11 -0
  24. data/lib/binder_core/registry.rb +45 -0
  25. data/lib/binder_core/scanner.rb +36 -0
  26. data/lib/binder_core/settings.rb +23 -0
  27. data/lib/binder_core/version.rb +3 -0
  28. data/lib/binder_core.rb +44 -0
  29. data/spec/binder_core/asset_spec.rb +64 -0
  30. data/spec/binder_core/binder_spec.rb +48 -0
  31. data/spec/binder_core/compiler_spec.rb +132 -0
  32. data/spec/binder_core/config_spec.rb +146 -0
  33. data/spec/binder_core/console_spec.rb +60 -0
  34. data/spec/binder_core/debug/stack_spec.rb +90 -0
  35. data/spec/binder_core/defaults_spec.rb +43 -0
  36. data/spec/binder_core/definition_spec.rb +20 -0
  37. data/spec/binder_core/file_context_spec.rb +79 -0
  38. data/spec/binder_core/file_ref_spec.rb +43 -0
  39. data/spec/binder_core/folder_context_spec.rb +104 -0
  40. data/spec/binder_core/parser_spec.rb +72 -0
  41. data/spec/binder_core/parsers/asset_parser_spec.rb +14 -0
  42. data/spec/binder_core/parsers/folder_parser_spec.rb +15 -0
  43. data/spec/binder_core/parsers/null_parser_spec.rb +9 -0
  44. data/spec/binder_core/parsers/text_parser_spec.rb +16 -0
  45. data/spec/binder_core/registry_spec.rb +61 -0
  46. data/spec/binder_core/scanner_spec.rb +79 -0
  47. data/spec/binder_core/settings_spec.rb +32 -0
  48. data/spec/binder_core_spec.rb +87 -0
  49. data/spec/spec_helper.rb +19 -0
  50. data/spec/support/content/test/_hidden_file.txt +1 -0
  51. data/spec/support/content/test/_hidden_folder/childofhiddenfolder.txt +1 -0
  52. data/spec/support/content/test/emptyfolder/_ignore.txt +1 -0
  53. data/spec/support/content/test/logo.png +0 -0
  54. data/spec/support/content/test/subfolder/subtext.txt +1 -0
  55. data/spec/support/content/test/testfile.txt +5 -0
  56. data/spec/support/content/test/unknown.tmp +1 -0
  57. data/spec/support/parsers/test_parsers.rb +20 -0
  58. metadata +147 -0
@@ -0,0 +1,38 @@
1
+ # hook to help the api appear cleaner when dealing with file objects
2
+ module BinderCore
3
+ class FileRef
4
+ attr_accessor :path
5
+ def initialize path = nil
6
+ @path = path
7
+ end
8
+
9
+ def name
10
+ File.basename( @path, File.extname(@path) )
11
+ end
12
+
13
+ def full_name
14
+ File.basename( @path )
15
+ end
16
+
17
+ def ext
18
+ File.extname(@path)
19
+ end
20
+
21
+ def dir?
22
+ File.directory?(@path)
23
+ end
24
+
25
+ def files
26
+ # return an array of File objects for each file in this directory
27
+ # if it is not at Directory return an empty array
28
+ fls = []
29
+ if self.dir?
30
+ Dir.foreach(@path) do |fname|
31
+ next if fname == '.' or fname == '..'
32
+ fls << fname
33
+ end
34
+ end
35
+ fls
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,55 @@
1
+ module BinderCore
2
+ class FolderContext
3
+ attr_accessor :parsers, :rules, :file, :route, :data, :assets, :console, :params
4
+
5
+ def initialize
6
+ @rules = []
7
+ @parsers = []
8
+ @route = []
9
+ @assets = []
10
+ @params = {}
11
+ end
12
+
13
+ def raw
14
+ {:data => data,:assets => assets}
15
+ end
16
+
17
+ def parse_files
18
+ file.files.each do |fname|
19
+ # allow the console.continue? flag to halt the compile process
20
+ break unless console.continue?
21
+
22
+ file_config = lambda do |config|
23
+ config.set_route @route.dup
24
+ config.set_path File.join(file.path, fname)
25
+ config.add_params @params.dup
26
+ config.set_console console
27
+ config.set_rules @rules.dup
28
+ config.set_parsers @parsers.dup
29
+ config.set_assets @assets
30
+ config.verify_context
31
+ end
32
+
33
+ parsed_file = Scanner.scan file_config
34
+ if parsed_file.data then add_data_from( parsed_file ) end
35
+ end
36
+ @data = expand_single_keys @data unless @data.nil?
37
+ raw
38
+ end
39
+
40
+ private
41
+ def add_data_from( file_context )
42
+ @data ||= {}
43
+ key = file_context.name
44
+ if @data.has_key? key then @data[key] << file_context.data
45
+ else @data[key] = [file_context.data]
46
+ end
47
+ end
48
+ def expand_single_keys( hash )
49
+ hash.keys.each do |key|
50
+ hash[key] = hash[key][0] if hash[key].length == 1
51
+ end
52
+ hash
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,82 @@
1
+ module BinderCore
2
+ # This is the base class for all parsers.
3
+ # When it is instanciated it is assigned one FileContext, which may be a file or a folder.
4
+ # Subclasses must define a parse method which is called when the time is right.
5
+ #
6
+ # If the parse method doesn't actively add data, create a new asset and/or descend into a folder
7
+ # then nothing gets added to the binder data and the scanner essentaily skips this file/folder.
8
+ #
9
+ # Hence the NullParser implmentation:
10
+ #
11
+ # class NullParser < BinderCore::Parser
12
+ # def parse
13
+ # end
14
+ # end
15
+ #
16
+ class Parser
17
+ def initialize(file_context)
18
+ @context = file_context
19
+ end
20
+
21
+ # Access FileRef object associated with this Parser
22
+ def file
23
+ @context.file
24
+ end
25
+
26
+ # if you dont supply a second argument to the 'add' method
27
+ # this is the key that will be used when adding a data value to the parent hash
28
+ def key=(val)
29
+ @context.name = val
30
+ end
31
+
32
+ def key
33
+ @context.name
34
+ end
35
+
36
+ # adds a data value object to the parent hash
37
+ # you can specify the key if you supply a second parameter, otherwise 'self.key' is used
38
+ def add(data, key = "")
39
+ if !key.empty? then @context.name = key end
40
+ @context.data = data
41
+ end
42
+
43
+ # This creates a new asset object and adds it to the binder asset bucket.
44
+ # You have the option of supplying a path to a file, otherwise it creates an asset using
45
+ # the current file path
46
+ def new_asset(path = nil)
47
+ path ||= @context.file.path
48
+ @context.register_asset BinderCore::Asset.new( path )
49
+ end
50
+
51
+ # The console is used for debugging, it allows you to log messages and warnings.
52
+ # You can also trigger an internal error halting the parsing process.
53
+ # Check out BinderCore::Console
54
+ def console
55
+ @context.console
56
+ end
57
+
58
+ # Access to arbitary user params inherited from the parent context.
59
+ # Any changes you make to this will be similarly inherited by direct children using a shallow duplicate.
60
+ def params
61
+ @context.params
62
+ end
63
+
64
+ # Instructs the compiler to decend into this folder and scan.
65
+ # You can override the path in the config block if you want to direct the compiler to a folder elsewhere.
66
+ # It will happily scan a different folder without disrupting the rest of the process.
67
+ #
68
+ # The config block works the same as the one used when you defined the binder. This means that you can
69
+ # add new parser definitions which will only be applied to children of the folder... powerful stuff!
70
+ #
71
+ # This method returns the data gleemed from that scanning process, you need to
72
+ # add it to make it part of the binder data, it will not do so automatically!
73
+ #
74
+ # On the other hand, assets ARE automatically added to the binder asset bucket when created.
75
+ #
76
+ def descend(&config)
77
+ config ||= lambda { |c| }
78
+ raw = @context.descend &config
79
+ raw[:data]
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,7 @@
1
+ module BinderCore
2
+ class AssetParser < BinderCore::Parser
3
+ def parse
4
+ add new_asset
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module BinderCore
2
+ class FolderParser < BinderCore::Parser
3
+ def parse
4
+ add descend
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,6 @@
1
+ module BinderCore
2
+ class NullParser < BinderCore::Parser
3
+ def parse
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,11 @@
1
+ module BinderCore
2
+ class TextParser < BinderCore::Parser
3
+ def parse
4
+ add read_utf
5
+ end
6
+
7
+ def read_utf
8
+ File.open( self.file.path, 'r:UTF-8') { |f| f.read }
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,45 @@
1
+ module BinderCore
2
+ class Registry
3
+ include Enumerable
4
+
5
+ def initialize(name)
6
+ @name = name
7
+ @items = {}
8
+ end
9
+
10
+ def add(item)
11
+ add_as(item.name, item)
12
+ item
13
+ end
14
+
15
+ def find(name)
16
+ @items[name.to_sym] or raise ArgumentError.new("#{@name} not registered: #{name.to_s}")
17
+ end
18
+
19
+ def each(&block)
20
+ @items.values.uniq.each(&block)
21
+ end
22
+
23
+ def [](name)
24
+ find(name)
25
+ end
26
+
27
+ def registered?(name)
28
+ @items.key?(name.to_sym)
29
+ end
30
+
31
+ def clear
32
+ @items.clear
33
+ end
34
+
35
+ private
36
+
37
+ def add_as(name, item)
38
+ if registered?(name)
39
+ raise DuplicateDefinitionError, "#{@name} already registered: #{name}"
40
+ else
41
+ @items[name.to_sym] = item
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,36 @@
1
+ module BinderCore
2
+ class Scanner
3
+ # Parse a file obtaining a parser from the configured definitions.
4
+ # Returns a parsed file context
5
+ def self.scan(config)
6
+ # configure the context
7
+ context = FileContext.new
8
+ config.call Config.new context
9
+ context.console.stack.add_file context.route, context
10
+ # parse the file
11
+ file_parser = get_parser_for context
12
+ context.console.stack.add_parser context.route, file_parser
13
+ file_parser.parse
14
+ # return parsed context
15
+ context
16
+ end
17
+
18
+ # Parse all files in a directory
19
+ def self.descend(config)
20
+ context = FolderContext.new
21
+ config.call Config.new context
22
+ context.console.stack.add_folder context.route, context
23
+ context.parse_files
24
+ end
25
+
26
+ private
27
+ # Runs the configured file context against all the rule and parser
28
+ # definitions test method and returns an instance of the first one that matches
29
+ def self.get_parser_for file_context
30
+ [*file_context.rules,*file_context.parsers].each do |defn|
31
+ parser = defn.test( file_context )
32
+ return parser.new( file_context ) if parser.kind_of?(Class)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,23 @@
1
+ module BinderCore
2
+ class Settings
3
+
4
+ def self.configure(&block)
5
+ instance_eval &block
6
+ end
7
+
8
+ def self.config
9
+ @config ||= Config.new
10
+ end
11
+
12
+ def self.propegate
13
+ config.clone
14
+ end
15
+
16
+ class Config
17
+ attr_accessor :base_asset_url, :content_folder, :folder_size_limit_mb, :base_asset_folder
18
+ def prop_list
19
+ (self.public_methods - [*Object.public_methods,:prop_list]).sort
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,3 @@
1
+ module BinderCore
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,44 @@
1
+ require "binder_core/version"
2
+ require "binder_core/binder"
3
+ require "binder_core/compiler"
4
+ require "binder_core/registry"
5
+ require "binder_core/scanner"
6
+ require "binder_core/config"
7
+ require "binder_core/folder_context"
8
+ require "binder_core/file_context"
9
+ require "binder_core/file_ref"
10
+ require "binder_core/definition"
11
+ require "binder_core/asset"
12
+ require "binder_core/parser"
13
+ require "binder_core/defaults"
14
+ require "binder_core/settings"
15
+ require "binder_core/console"
16
+
17
+ module BinderCore
18
+
19
+ # Raised when a binder is defined with the same name as a previously-defined binder.
20
+ class DuplicateDefinitionError < RuntimeError; end
21
+
22
+ # Raised when a context object is found to be invalid after it is expected to be fully configured
23
+ class InvalidContextError < RuntimeError; end
24
+
25
+ def self.binders
26
+ @binders ||= BinderCore::Registry.new("Binder")
27
+ end
28
+
29
+ def self.define(name, &config)
30
+ bndr = BinderCore::Binder.new(name, config)
31
+ binders.add( bndr );
32
+ end
33
+
34
+ def self.binder_by_name(name)
35
+ binders[name]
36
+ end
37
+
38
+ def self.compile(name)
39
+ binder = binder_by_name(name)
40
+ if binder then Compiler.compile binder end
41
+ binder
42
+ end
43
+ end
44
+
@@ -0,0 +1,64 @@
1
+ require 'spec_helper'
2
+
3
+ describe BinderCore::Asset do
4
+ let(:path) { "pathto/image.jpg" }
5
+ let(:cxt_route) { %w{work gallery} }
6
+ let(:asset) { BinderCore::Asset.new path }
7
+ let(:base_url) { "http://www.example.com" }
8
+
9
+ before(:each) do
10
+ asset.set_route cxt_route
11
+ asset.base_url = base_url
12
+ end
13
+
14
+ it "should have a file and a feed route" do
15
+ asset.file.path.should == path
16
+ asset.route.should == cxt_route
17
+ end
18
+
19
+ it "should provide a feed_path string" do
20
+ asset.feed_path.should == [*cxt_route,"image"].join("/")
21
+ end
22
+
23
+ it "should have the name of the file as its name by default" do
24
+ asset.name.should == "image"
25
+ end
26
+
27
+ it "should change the feed path when the name changes" do
28
+ asset.name = "image_1"
29
+ asset.feed_path.should == [*cxt_route,"image_1"].join("/")
30
+ end
31
+
32
+ it "should set the asset with_name()" do
33
+ asset.with_name("image_2")
34
+ asset.feed_path.should == [*cxt_route,"image_2"].join("/")
35
+ end
36
+
37
+ it "should have a base_url property" do
38
+ asset.should respond_to(:base_url)
39
+ asset.base_url = "test"
40
+ asset.base_url.should == "test"
41
+ end
42
+
43
+ it "should have a asset_url property" do
44
+ asset.should respond_to(:asset_url)
45
+ asset.asset_url = "test"
46
+ asset.asset_url.should == "test"
47
+ end
48
+
49
+ it "should create asset_url automatically when the route or the base_url is updated" do
50
+ asset.asset_url.should == File.join( base_url, [*asset.route,asset.file.full_name].join("/"))
51
+ new_route = asset.set_route ["feed","folder","folder2"]
52
+ asset.asset_url.should == File.join( base_url, [*new_route,asset.file.full_name].join("/"))
53
+ asset.base_url = new_base_url = "http://example2.com"
54
+ asset.asset_url.should == File.join( new_base_url, [*new_route,asset.file.full_name].join("/"))
55
+ end
56
+
57
+ it "should not allow you to tamper with the route array directly" do
58
+ lambda { asset.route << "test" }.should raise_error(RuntimeError)
59
+ end
60
+
61
+ it "should raise an exception when you try to assign an empty string as the asset name" do
62
+ expect { asset.with_name("") }.to raise_error( ArgumentError, "Asset name for '#{asset.file.path}' cannot be empty")
63
+ end
64
+ end
@@ -0,0 +1,48 @@
1
+ require "spec_helper"
2
+
3
+ describe "Binder" do
4
+ before(:each) do
5
+ @name = :test
6
+ @data = {:test => "test value" }
7
+ @binder = BinderCore::Binder.new(@name)
8
+ @binder.raw = @data;
9
+ end
10
+
11
+ it "should have a binder name" do
12
+ @binder.name.should == @name
13
+ end
14
+
15
+ it "should has a console property" do
16
+ @binder.should respond_to(:console)
17
+ @binder.should respond_to(:init_console)
18
+ end
19
+
20
+ it "should create another binder with a different name" do
21
+ name2 = :test2
22
+ binder2 = BinderCore::Binder.new(name2)
23
+ binder2.name.should == name2
24
+ end
25
+
26
+ it "should have a config proc" do
27
+ @binder.config.should be_kind_of(Proc)
28
+ end
29
+
30
+ it "should supply a data hash when raw is called" do
31
+ @binder.raw[:test].should == @data[:test]
32
+ end
33
+
34
+ it "should call the user config block after its own configuration is done" do
35
+ path = "../content/test/"
36
+ binder = BinderCore::Binder.new(@name, lambda { |config| config.set_path( path ) } )
37
+ config = double("Config")
38
+ config.should_receive(:set_path).with(path).ordered
39
+ binder.config.call(config)
40
+ end
41
+
42
+ it "should call Compiler.link_assets with the raw data object and return final compiled object with data and assets" do
43
+ @binder.raw = {:data => @data }
44
+ @binder.raw[:data].should == @data
45
+ BinderCore::Compiler.should_receive(:link_assets).with(@data).and_return(@data)
46
+ @binder.link_assets.should == {:data => @data, :assets => [] }
47
+ end
48
+ end
@@ -0,0 +1,132 @@
1
+ require 'spec_helper'
2
+
3
+ describe BinderCore::Compiler do
4
+ describe "#compile" do
5
+ let(:binder) { double("BinderMock").as_null_object }
6
+ let(:stack) { double("StackMock").as_null_object }
7
+ let(:console) { double("ConsoleMock", :stack => stack ) }
8
+ let(:config) { double("ConfigMock").as_null_object }
9
+ let(:data) { {:key => "value"} }
10
+ let(:assets) { ["assets"] }
11
+ let(:file) { double("FileContextMock", :data => data, :assets => assets) }
12
+
13
+ before(:each) do
14
+ BinderCore::Scanner.should_receive(:scan) do |con|
15
+ con.call(config)
16
+ file
17
+ end
18
+ end
19
+
20
+ after(:each) do
21
+ BinderCore::Compiler.compile binder
22
+ end
23
+
24
+ it "should copy compiler settings from the global compiler defaults" do
25
+ settings = double("SettingsMock")
26
+ BinderCore::Settings.should_receive(:propegate).and_return settings
27
+ BinderCore::Console.should_receive(:new).with(settings).and_return console
28
+ binder.should_receive(:init_console).with(console)
29
+ end
30
+
31
+ it "should assemble pre-config, default config, and user defined config procs" do
32
+ default = double("DefaultProcMock")
33
+ user = double("UserProcMock")
34
+ BinderCore::Default.should_receive(:config).and_return(default)
35
+ binder.should_receive(:config).and_return(user)
36
+ config.should_receive(:set_console)
37
+ default.should_receive(:call)
38
+ user.should_receive(:call)
39
+ end
40
+
41
+ it "should add data to the binder raw property" do
42
+ binder.should_receive(:raw=).with({:data => data, :assets => assets })
43
+ end
44
+ end
45
+
46
+ describe "#link" do
47
+ let(:asset_path) { "path/to/asset.jpg" }
48
+ let(:asset) { BinderCore::Asset.new(asset_path) }
49
+ let(:route) { ["work","gallery"] }
50
+ let(:base) { "http://www.example.com/binder/" }
51
+ let(:raw_data) { {:gallery => {:image => asset, :images => [asset,"not_asset",asset], :description => "This is a description"} } }
52
+
53
+ before(:each) do
54
+ asset.set_route route
55
+ asset.base_url = base
56
+ @res = BinderCore::Compiler.link_assets(raw_data)
57
+ end
58
+
59
+ it "should replace the asset objects within the nested gallery hash" do
60
+ @res[:gallery][:image].should == asset.asset_url
61
+ end
62
+
63
+ it "should replace all assets within a nexted images array" do
64
+ @res[:gallery][:images].should == [asset.asset_url,"not_asset",asset.asset_url]
65
+ end
66
+
67
+ it "should not effect the raw_data object" do
68
+ @res.should_not == raw_data
69
+ end
70
+ end
71
+
72
+ describe "#configure" do
73
+ it "should call the Default.settings function" do
74
+ BinderCore::Default.should_receive(:settings)
75
+ BinderCore::Compiler.configure {}
76
+ end
77
+
78
+ it "should configure the settings object" do
79
+ BinderCore::Settings.should_receive(:configure).twice
80
+ BinderCore::Compiler.configure do
81
+ config.base_asset_url = url
82
+ config.content_folder = path
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ describe BinderCore::Compiler::Iterator do
89
+ let(:itr) { BinderCore::Compiler::Iterator.new }
90
+ let(:arr) { ["a",1,{}] }
91
+ let(:hash) { {:key1 => arr[0], :key2 => arr[1], :key3 => arr[2] } }
92
+
93
+ it "should iterate objects in an array" do
94
+ itr.init arr
95
+ i = 0
96
+ itr.count.should == arr.count
97
+ itr.has_next?.should be_true
98
+ while itr.has_next?
99
+ itr.next.should == arr[i]
100
+ i += 1
101
+ end
102
+ end
103
+
104
+ it "should iterate of values in a hash" do
105
+ itr.init hash
106
+ i = 0
107
+ itr.count.should == arr.count
108
+ itr.has_next?.should be_true
109
+ while itr.has_next?
110
+ itr.next.should == arr[i]
111
+ i += 1
112
+ end
113
+ end
114
+
115
+ it "should replace elements in a hash" do
116
+ itr.init hash
117
+ while itr.has_next?
118
+ itr.next
119
+ itr.replace "replaced"
120
+ end
121
+ hash.should == {:key1 => "replaced", :key2 => "replaced", :key3 => "replaced" }
122
+ end
123
+
124
+ it "should replace elements in an array" do
125
+ itr.init arr
126
+ while itr.has_next?
127
+ itr.next
128
+ itr.replace "replaced"
129
+ end
130
+ arr.should == ["replaced","replaced","replaced"]
131
+ end
132
+ end