noumenon 0.0.3 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. data/.gitignore +3 -0
  2. data/.travis.yml +0 -4
  3. data/.yardopts +5 -0
  4. data/Gemfile +1 -3
  5. data/README.md +57 -80
  6. data/Rakefile +17 -6
  7. data/bin/noumenon +6 -0
  8. data/features/dynamic_template_rendering.feature +107 -0
  9. data/features/generator/site_generator.feature +25 -0
  10. data/features/mounted_applications.feature +30 -0
  11. data/features/static_template_rendering.feature +43 -0
  12. data/features/step_definitions/asset_steps.rb +7 -0
  13. data/features/step_definitions/content_steps.rb +7 -0
  14. data/features/step_definitions/generator_steps.rb +22 -0
  15. data/features/step_definitions/request_steps.rb +31 -0
  16. data/features/step_definitions/theme_steps.rb +19 -0
  17. data/features/support/env.rb +38 -0
  18. data/features/support/theme/theme.yml +5 -0
  19. data/features/theme_assets.feature +22 -0
  20. data/generators/repository/index.yml +3 -0
  21. data/generators/site/Gemfile +3 -0
  22. data/generators/site/config.ru +7 -0
  23. data/generators/theme/assets/style.css +1 -0
  24. data/generators/theme/layouts/default.nou.html +23 -0
  25. data/generators/theme/templates/default.nou.html +12 -0
  26. data/generators/theme/theme.yml +5 -0
  27. data/lib/noumenon/cli.rb +27 -0
  28. data/lib/noumenon/core.rb +70 -77
  29. data/lib/noumenon/repository/file_system.rb +102 -0
  30. data/lib/noumenon/repository.rb +39 -0
  31. data/lib/noumenon/spec/example_app.rb +19 -6
  32. data/lib/noumenon/spec/theme_helpers.rb +66 -0
  33. data/lib/noumenon/spec.rb +4 -7
  34. data/lib/noumenon/string_extensions.rb +21 -0
  35. data/lib/noumenon/template.rb +113 -72
  36. data/lib/noumenon/theme/assets_middleware.rb +21 -0
  37. data/lib/noumenon/theme.rb +106 -0
  38. data/lib/noumenon/version.rb +3 -1
  39. data/lib/noumenon.rb +68 -100
  40. data/noumenon.gemspec +13 -9
  41. data/spec/noumenon/repository/file_system_spec.rb +115 -0
  42. data/spec/noumenon/repository_spec.rb +40 -0
  43. data/spec/noumenon/template_spec.rb +9 -7
  44. data/spec/noumenon/theme_spec.rb +129 -0
  45. data/spec/noumenon_spec.rb +24 -80
  46. data/spec/spec_helper.rb +5 -14
  47. data/spec/support/file_matchers.rb +45 -0
  48. data/spec/support/templates/basic_template.html +1 -0
  49. data/spec/{fixtures/themes/example_without_layout → support}/templates/template_with_fields.html +1 -1
  50. metadata +143 -62
  51. data/lib/noumenon/configuration.rb +0 -28
  52. data/lib/noumenon/spec/fixtures.rb +0 -34
  53. data/spec/fixtures/fixture_specs/test +0 -1
  54. data/spec/fixtures/missing_application/mounted_app/config.yml +0 -1
  55. data/spec/fixtures/static_example/directory_with_index/index.yml +0 -1
  56. data/spec/fixtures/static_example/found.html +0 -1
  57. data/spec/fixtures/static_example/liquid_example.html +0 -1
  58. data/spec/fixtures/static_example/mounted_app/config.yml +0 -1
  59. data/spec/fixtures/static_example/template_with_substitutions.html +0 -1
  60. data/spec/fixtures/static_example/templates/basic_example.yml +0 -1
  61. data/spec/fixtures/static_example/templates/with_fields.yml +0 -2
  62. data/spec/fixtures/themes/example/assets/example.txt +0 -1
  63. data/spec/fixtures/themes/example/templates/layout.html +0 -3
  64. data/spec/fixtures/themes/example_without_layout/templates/basic_template.html +0 -1
  65. data/spec/fixtures/themes/example_without_layout/templates/fields.html +0 -1
  66. data/spec/fixtures/themes/external_theme/assets/example.txt +0 -1
  67. data/spec/fixtures/themes/unregistered/lib/unregistered.rb +0 -1
  68. data/spec/noumenon/config_spec.rb +0 -29
  69. data/spec/noumenon/core_spec.rb +0 -105
  70. data/spec/noumenon/spec/example_app_spec.rb +0 -14
  71. data/spec/noumenon/spec/fixtures_spec.rb +0 -41
  72. data/spec/noumenon/spec_spec.rb +0 -7
  73. data/watchr.rb +0 -2
@@ -1,88 +1,129 @@
1
+ require 'noumenon'
1
2
  require 'liquid'
2
3
 
3
- module Noumenon
4
- # Templates specify the structure and presentation of a particular piece of content in
5
- # a Noumenon site, and are usually provided by a theme.
4
+ # Templates specify the structure and presentation of a particular piece of content in
5
+ # a Noumenon site, and are usually provided by a theme.
6
+ #
7
+ # A template is split into two parts, the metadata, which defines what data is needed for
8
+ # the template, and a Liquid template, which defines how the data should be presented. When
9
+ # loaded from a file, a template will look like this:
10
+ #
11
+ # title:
12
+ # label: Page title
13
+ # required: true
14
+ # type: string
15
+ # help: The title displayed at the top of the page
16
+ # author:
17
+ # label: Author
18
+ # required: false
19
+ # type: string
20
+ # help: The author of the article. If not provided, no byline will be shown.
21
+ # body:
22
+ # label: Body text
23
+ # required: true
24
+ # type: text
25
+ # help: The main article body. Will be processed with Textile for formatting.
26
+ # ---
27
+ # <h1>{{ title }}</h1>
28
+ # {% if author %}
29
+ # <p class="byline">By {{ author }}</p>
30
+ # {% endif %}
31
+ # {{ body | textilize }}
32
+ #
33
+ # And can be rendered like this:
34
+ #
35
+ # Template.from_file("/path/to/template").render("title" => "An Example Page", "author" => "Jon Wood", "body" => "This is an article...")
36
+ #
37
+ # If any required fields are missing from the data provided then a MissingContentError will be raised.
38
+ #
39
+ # @api public
40
+ class Noumenon::Template
41
+ # Indicates one or more required fields were not provided to the template.
6
42
  #
7
- # A template is split into two parts, the metadata, which defines what data is needed for
8
- # the template, and a Liquid template, which defines how the data should be presented. When
9
- # loaded from a file, a template will look like this:
43
+ # The missing fields are listed in the error message.
10
44
  #
11
- # title:
12
- # label: Page title
13
- # required: true
14
- # type: string
15
- # help: The title displayed at the top of the page
16
- # author:
17
- # label: Author
18
- # required: false
19
- # type: string
20
- # help: The author of the article. If not provided, no byline will be shown.
21
- # body:
22
- # label: Body text
23
- # required: true
24
- # type: text
25
- # help: The main article body. Will be processed with Textile for formatting.
26
- # ---
27
- # <h1>{{ title }}</h1>
28
- # {% if author %}
29
- # <p class="byline">By {{ author }}</p>
30
- # {% endif %}
31
- # {{ body | textilize }}
32
- #
33
- # And can be rendered like this:
45
+ # @api public
46
+ class MissingContentError < StandardError; end
47
+
48
+ # Indicates the requested template could not be found.
34
49
  #
35
- # Template.from_file("/path/to/template").render("title" => "An Example Page", "author" => "Jon Wood", "body" => "This is an article...")
36
- #
37
- # If any required fields are missing from the data provided then a MissingContentError will be raised.
38
- class Template
39
- class MissingContentError < StandardError; end
40
- class NotFoundError < StandardError; end
41
-
42
- # The location this template was loaded from.
43
- attr_accessor :source
50
+ # @api public
51
+ class NotFoundError < StandardError; end
52
+
53
+ # The location this template was loaded from.
54
+ # @api public
55
+ attr_accessor :source
44
56
 
45
- # The template view.
46
- attr_accessor :content
57
+ # The template view.
58
+ # @api public
59
+ attr_accessor :content
47
60
 
48
- # The fields used by this template.
49
- attr_accessor :fields
61
+ # The fields used by this template.
62
+ # @api public
63
+ attr_accessor :fields
64
+
65
+ # Loads a template from the specified path.
66
+ #
67
+ # @api public
68
+ #
69
+ # @param [ String, #to_s ] path the file to load the template from
70
+ # @raise [ Noumenon::Template::NotFoundError ] the template could not be found
71
+ # @return [ Noumenon::Template ] the loaded template
72
+ # @api public
73
+ def self.from_file(path)
74
+ raise NotFoundError.new("No template could be found at #{path}.") unless File.exists?(path)
50
75
 
51
- # Loads a template from the specified path.
52
- #
53
- # If the file does not exist then a NotFoundError will be raised.
54
- def self.from_file(path)
55
- raise NotFoundError.new("No template could be found at #{path}.") unless File.exists?(path)
56
-
57
- content = File.read(path)
76
+ content = File.read(path)
58
77
 
59
- parts = content.split("\n---\n")
60
- if parts.size == 1
61
- fields = {}
62
- else
63
- fields = YAML.load(parts.first)
64
- content = parts.last
65
- end
66
-
67
- self.new(path, content, fields)
78
+ parts = content.split("\n---\n")
79
+ if parts.size == 1
80
+ fields = {}
81
+ else
82
+ fields = YAML.load(parts.first)
83
+ content = parts.last
68
84
  end
69
85
 
70
- def initialize(source = nil, content = nil, fields = {})
71
- @source = source
72
- @content = content
73
- @fields = fields
74
- end
86
+ self.new(path, content, fields)
87
+ end
88
+
89
+ # Creates a new Template instance.
90
+ #
91
+ # @api public
92
+ #
93
+ # @param [ #to_s ] source the location the template was loaded from
94
+ # @param [ #to_s ] content the Liquid template to render
95
+ # @param [ Hash, #each ] fields the list of fields used within the template
96
+ #
97
+ # @example Loading a template from a database
98
+ # fields = load_field_rows.inject({}) do |fields, row|
99
+ # fields[row[:name]] = { "required" => row[:required], "type" => row[:type] }
100
+ # end
101
+ #
102
+ # Noumenon::Template.new("mysql://localhost/database/table#row_32", row[:content], fields)
103
+ #
104
+ # @api public
105
+ def initialize(source = nil, content = nil, fields = {})
106
+ @source = source
107
+ @content = content
108
+ @fields = fields
109
+ end
110
+
111
+ # Renders the template.
112
+ #
113
+ # @param [ Hash, #each ] page_content the content to provide to the template
114
+ # @raise [ Noumenon::Template::MissingContentError ] one or more required fields were not provided
115
+ # @return [ #to_s ] the rendered template
116
+ # @api public
117
+ def render(page_content = {})
118
+ fields_from_page = page_content.stringify_keys
75
119
 
76
- # Renders the template. If any fields are missing from page_content then a MissingContentError will be raised.
77
- def render(page_content = {})
78
- missing_fields = []
79
- fields.each do |key, field|
80
- missing_fields << key if field["required"] && !page_content.key?(key)
81
- end
120
+ missing_fields = []
121
+ fields.each do |key, field|
122
+ missing_fields << key if field["required"] && !fields_from_page.key?(key)
123
+ end
82
124
 
83
- raise MissingContentError.new("The following fields were missing from your content: #{missing_fields.sort.join(", ")}") unless missing_fields.empty?
125
+ raise MissingContentError.new("The following fields were missing from your content: #{missing_fields.sort.join(", ")}") unless missing_fields.empty?
84
126
 
85
- Liquid::Template.parse(content).render(page_content)
86
- end
127
+ Liquid::Template.parse(content).render(fields_from_page)
87
128
  end
88
129
  end
@@ -0,0 +1,21 @@
1
+ require 'noumenon/theme'
2
+ require 'sinatra'
3
+
4
+ # Handles requests for assets within loaded themes.
5
+ #
6
+ # Any request to /themes/Theme Name/* will be served from the theme's assets directory,
7
+ # with existing files being served, and non-existant files generating a 404.
8
+ #
9
+ # The middleware is automatically added to the stack if you're running using Noumenon.server
10
+ #
11
+ # @api public
12
+ class Noumenon::Theme::AssetsMiddleware < Sinatra::Base
13
+ get '/themes/:theme/*' do |theme, asset|
14
+ halt 404, "File Not Found" unless Noumenon::Theme.themes.key? theme
15
+
16
+ asset_path = File.join(Noumenon::Theme.themes[theme].path, "assets", asset)
17
+ halt 404, "File Not Found" unless File.exist?(asset_path)
18
+
19
+ send_file(asset_path)
20
+ end
21
+ end
@@ -0,0 +1,106 @@
1
+ require 'noumenon'
2
+ require 'yaml'
3
+
4
+ # Provides access to a theme and it's contents.
5
+ #
6
+ # @api public
7
+ class Noumenon::Theme
8
+ autoload :AssetsMiddleware, 'noumenon/theme/assets_middleware'
9
+
10
+ # The specified theme could not be loaded, either because the path does not exist, or it does not
11
+ # contain a theme.yml file.
12
+ #
13
+ # @api public
14
+ class NotFoundError < RuntimeError; end
15
+
16
+ # Load a theme from the specified path. Metadata about the theme will be laoded from "theme.yml",
17
+ # which should have the format shown in the example.
18
+ #
19
+ # If the theme is loaded succesfully it will also be registered in the theme list.
20
+ #
21
+ # @param [ String, #to_s ] path The path to load from.
22
+ # @raise [ Noumenon::Theme::NotFoundError ]
23
+ # @return [ Noumenon::Theme ] A populated theme.
24
+ # @see Noumenon::Theme.themes
25
+ def self.load(path)
26
+ unless File.exist?("#{path}/theme.yml")
27
+ raise NotFoundError.new("No theme was found at #{path}. Did you create a theme.yml file?")
28
+ end
29
+
30
+ description = YAML.load(File.read("#{path}/theme.yml"))
31
+ theme = Noumenon::Theme.new(path, description)
32
+
33
+ themes[theme.name] = theme
34
+ theme
35
+ end
36
+
37
+ # @return [ Hash ] A hash containing any loaded themes, keyed by their name.
38
+ # @api public
39
+ def self.themes
40
+ @themes ||= {}
41
+ end
42
+
43
+ # The path the theme was loaded from.
44
+ # @api public
45
+ attr_reader :path
46
+
47
+ # The name to refer to this theme by. Loaded from theme.yml.
48
+ # @api public
49
+ attr_accessor :name
50
+
51
+ # The author of this theme. Loaded from theme.yml.
52
+ # @api public
53
+ attr_accessor :author
54
+
55
+ # The email address to contact regarding this theme. Loaded from theme.yml.
56
+ # @api public
57
+ attr_accessor :email
58
+
59
+ # The copyright line to attribute this theme to. Loaded from theme.yml.
60
+ # @api public
61
+ attr_accessor :copyright
62
+
63
+ # The license this theme is distributed under. Loaded from theme.yml.
64
+ # @api public
65
+ attr_accessor :license
66
+
67
+ # Create a new them instance.
68
+ #
69
+ # @param [ String, #to_s ] path The path the theme was loaded from.
70
+ # @param [ Hash ] description Any metadata to attach to theme.
71
+ # @option description [ String ] :name The human readable name of the theme.
72
+ # @option description [ String ] :author The author of the theme.
73
+ # @option description [ String ] :email The email address to content regarding this theme.
74
+ # @option description [ String ] :copyright The copyright line to attribute this theme to.
75
+ # @option description [ String ] :license The license this theme is distributed under.
76
+ # @api public
77
+ def initialize(path, description = {})
78
+ @path = path
79
+
80
+ description.each do |name, value|
81
+ if respond_to? "#{name}="
82
+ send "#{name}=", value
83
+ end
84
+ end
85
+ end
86
+
87
+ # Load a template from the theme's templates directory.
88
+ #
89
+ # @param [ String, #to_s ] name The path to load the template from. This path will be prefixed with "templates/"
90
+ # @return [ Noumenon::Template ] The loaded template.
91
+ # @raise [ Noumenon::Template::NotFoundError ]
92
+ # @api public
93
+ def template(name)
94
+ Noumenon::Template.from_file File.join(path, "templates", name)
95
+ end
96
+
97
+ # Load a template from the theme's layouts directory.
98
+ #
99
+ # @param [ String, #to_s ] name The path to load the template from. This path will be prefixed with "layouts/"
100
+ # @return [ Noumenon::Template ] The loaded template.
101
+ # @raise [ Noumenon::Template::NotFoundError ]
102
+ # @api public
103
+ def layout(name)
104
+ Noumenon::Template.from_file File.join(path, "layouts", name)
105
+ end
106
+ end
@@ -1,3 +1,5 @@
1
1
  module Noumenon
2
- VERSION = "0.0.3"
2
+ # The current version of Noumenon.
3
+ # @api public
4
+ VERSION = "0.1.0"
3
5
  end
data/lib/noumenon.rb CHANGED
@@ -1,109 +1,77 @@
1
- require 'noumenon/configuration'
2
- require 'noumenon/template'
3
- require 'noumenon/core'
1
+ require 'noumenon/version'
2
+ require 'noumenon/string_extensions'
3
+ require 'facets'
4
+ require 'rack/builder'
4
5
 
6
+ # @api public
5
7
  module Noumenon
6
- class TemplateNotFoundError < StandardError; end
7
- class MissingApplicationError < StandardError; end
8
+ autoload :Core, 'noumenon/core'
9
+ autoload :Repository, 'noumenon/repository'
10
+ autoload :Template, 'noumenon/template'
11
+ autoload :Theme, 'noumenon/theme'
12
+ autoload :Spec, 'noumenon/spec'
8
13
 
9
- class << self
10
- # Returns a configured Noumenon application. Intended for use in a Rack configuration:
11
- #
12
- # By default the following URL handlers are put in place:
13
- #
14
- # */themes/#{theme_name}/*
15
- #
16
- # The assets directory of all installed themes are made accessible so that themes can
17
- # bundle stylesheets, javascripts, and other static assets.
18
- #
19
- # */*
20
- #
21
- # Anything below / is handled by Noumenon::Core, which will simply load a content item
22
- # if it exists, and render it within the specified template.
23
- #
24
- # *Configuring Custom URL Handlers*
25
- #
26
- # By placing a file called "config.yml" in a directory you can change the URL handler for
27
- # it. The "application" attribute sets the class that will handle that URL, for example:
28
- #
29
- # application: "Noumenon::Applications::ContactForm"
30
- #
31
- # Example:
32
- #
33
- # require 'noumenon'
34
- # Noumenon::Core.set :content_repository_path, "/srv/sites/example.org"
35
- #
36
- # run Noumenon.boot
37
- def boot
38
- Rack::Builder.new do
39
- Noumenon.themes.each do |name, details|
40
- map("/themes/#{name}") { run Rack::File.new(File.join(details[:path], "assets")) }
41
- end
42
-
43
- Dir.glob(File.join(Noumenon.config.content_repository_path, "**/config.yml")).each do |path|
44
- config = YAML.load(File.read(path))
45
-
46
- if config["application"]
47
- begin
48
- url = path.gsub(Noumenon.config.content_repository_path, "").split("/")[0..-2].join("/")
49
- app = Noumenon.constantize(config["application"]).new
50
-
51
- map(url) { run app }
52
- rescue NameError => e
53
- raise MissingApplicationError.new("The application #{config["application"]} has not been loaded.")
54
- end
55
- end
56
- end
57
-
58
- map("/") { run Core.new }
59
- end
60
- end
61
-
62
- # Attempts to convert a string into a constant.
63
- #
64
- # If the constant could not be found then NameError will be raised.
65
- def constantize(name)
66
- mod = Module
67
- name.split("::").each do |mod_name|
68
- mod = mod.const_get(mod_name.to_sym)
69
- end
70
-
71
- mod
14
+ # Starts Noumenon serving, this will usually be called from a config.ru file
15
+ # something like this:
16
+ #
17
+ # Noumenon.content_repository = Noumenon::Repository::FileSystem.new("/home/noumenon/content")
18
+ # Noumenon.theme = Noumenon::Theme.load("/home/noumenon/theme")
19
+ #
20
+ # run Noumenon.server
21
+ #
22
+ # While you can also just use an instance Noumenon::Core, keep in mind that it won't
23
+ # have the full Rack middleware stack, and so some functionality such as serving assets
24
+ # from themes won't be available.
25
+ #
26
+ # @api public
27
+ # @return [ Rack::Builder ] the Rack stack to serve a Noumenon site
28
+ def self.server
29
+ Rack::Builder.new do
30
+ use Noumenon::Theme::AssetsMiddleware
31
+ run Noumenon::Core
72
32
  end
33
+ end
34
+
35
+ # Returns the current content repository.
36
+ #
37
+ # @api public
38
+ # @return [ Noumenon::Repository, #get, #put ]
39
+ def self.content_repository
40
+ @content_repository
41
+ end
73
42
 
74
- # Provides access to the configuration of this application.
75
- #
76
- # If passed a block then the current configuration will be yielded in the form
77
- # of a Noumenon::Configuration instance, allowing it to be changed.
78
- #
79
- # Noumenon.config do |c|
80
- # c.theme = "example_theme"
81
- # end
82
- #
83
- def config
84
- @config ||= Configuration.new
85
- yield @config if block_given?
86
- @config
87
- end
88
- alias :configure :config
89
-
90
- # Set the application configuration
91
- #
92
- # Should be provided with something that implements the same interface as Noumenon::Configuration.
93
- def config=(config)
94
- @config = config
43
+ # Sets the repository to load site content from.
44
+ #
45
+ # @api public
46
+ # @param [ Noumenon::Repository, #get, #put ] repository the repository to use
47
+ def self.content_repository=(repository)
48
+ @content_repository = repository
49
+ end
50
+
51
+ # Returns the current theme
52
+ #
53
+ # @api public
54
+ # @return [ Noumenon::Theme ] the current theme
55
+ def self.theme
56
+ @theme
57
+ end
58
+
59
+ # Set the current theme.
60
+ #
61
+ # If provided with a [ Noumenon::Theme ] object then it will set that, otherwise
62
+ # it will convert the argument to a string, and attempt to find a loaded theme with
63
+ # that name.
64
+ #
65
+ # @api public
66
+ # @param [ Noumenon::Theme, #to_s ] theme either a theme object, or the name of a loaded theme
67
+ # @return [ Noumenon::Theme, nil ] the specified theme, or nil if it could not be found
68
+ def self.theme=(theme)
69
+ if theme.is_a? Noumenon::Theme
70
+ @theme = theme
71
+ else
72
+ self.theme = Noumenon::Theme.themes[theme]
95
73
  end
96
74
 
97
- # Returns details of any registered themes.
98
- def themes
99
- @themes ||= {}
100
- end
101
-
102
- # Registers a theme for use by Noumenon.
103
- #
104
- # Any files in the assets/ directory below the path provided will be accessible from /themes/theme_name.
105
- def register_theme(name, path)
106
- themes[name] = { :path => path }
107
- end
75
+ @theme
108
76
  end
109
77
  end
data/noumenon.gemspec CHANGED
@@ -9,10 +9,10 @@ Gem::Specification.new do |s|
9
9
  s.authors = ["Jon Wood"]
10
10
  s.email = ["jon@blankpad.net"]
11
11
  s.homepage = "https://github.com/Noumenon"
12
- s.summary = %q{An content management system backed by Git}
12
+ s.summary = %q{A flexible content management system.}
13
13
  s.description = <<EOF
14
- Noumenon is a content management system designed to be backed by a file system, with
15
- theme support.
14
+ Noumenon is a content management system designed to support being extended with Sinatra
15
+ applications.
16
16
 
17
17
  It's currently in an early stage of development, but right now you can create a basic
18
18
  static site using templates from a theme which specify the structure and presentation
@@ -22,9 +22,6 @@ templates.
22
22
  Future development will include an end-user friendly web interface for editing and
23
23
  creating content, while retaining the ability for developers and designers to manage
24
24
  the site's presentation using the tools they're most comfortable with.
25
-
26
- See http://github.com/Noumenon/example-app/ for a really bare bones example of how
27
- Noumenon works.
28
25
  EOF
29
26
 
30
27
  s.rubyforge_project = "noumenon"
@@ -34,9 +31,16 @@ EOF
34
31
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
35
32
  s.require_paths = ["lib"]
36
33
 
34
+ s.add_dependency "facets", ">= 2.9.1"
35
+ s.add_dependency "liquid", ">= 2.2"
37
36
  s.add_dependency "sinatra", ">= 1.2.3"
38
- s.add_dependency "liquid", ">= 2.2"
39
-
37
+
38
+ s.add_development_dependency "aruba", ">= 0.3.6"
39
+ s.add_development_dependency "capybara", ">= 1.0.0.beta1"
40
+ s.add_development_dependency "cucumber", ">= 0.10.2"
41
+ s.add_development_dependency "rake", ">= 0.9.0"
42
+ s.add_development_dependency "rdiscount", ">= 1.6.8"
40
43
  s.add_development_dependency "rspec", ">= 2.5.0"
41
- s.add_development_dependency "rack-test", ">= 0.5"
44
+ s.add_development_dependency "thor", ">= 0.14.6"
45
+ s.add_development_dependency "yard", ">= 0.7.1"
42
46
  end
@@ -0,0 +1,115 @@
1
+ require 'spec_helper'
2
+
3
+ describe Noumenon::Repository::FileSystem do
4
+ let(:content_path) { File.join(File.dirname(__FILE__), "..", "..", "..", "..", "tmp", "content") }
5
+
6
+ around do |example|
7
+ FileUtils.mkdir_p content_path
8
+ example.run
9
+ FileUtils.rm_r content_path
10
+ end
11
+
12
+ subject do
13
+ Noumenon::Repository::FileSystem.new path: content_path
14
+ end
15
+
16
+ describe "initialization" do
17
+ context "when no path option is provided" do
18
+ it "should raise an ArgumentError" do
19
+ lambda { Noumenon::Repository::FileSystem.new }.should raise_error ArgumentError
20
+ end
21
+
22
+ it "should provide details of the error of how to resolve the error" do
23
+ begin
24
+ Noumenon::Repository::FileSystem.new
25
+ rescue ArgumentError => e
26
+ e.to_s.should == "You must provide a path to the content repository: Noumenon::Repository::FileSystem.new(path: '/tmp')"
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ it { should respond_to(:put) }
33
+
34
+ describe "putting content in the repository" do
35
+ context "when writing to a top level file" do
36
+ it "creates a YAML file at the specified path" do
37
+ lambda { subject.put "example", { template: "static" } }.should create_file File.join(content_path, "example.yml")
38
+ end
39
+
40
+ it "writes the provided hash to the specified path" do
41
+ subject.put "example", { template: "static" }
42
+
43
+ YAML.load(File.read(File.join(content_path, "example.yml"))).should == { template: "static" }
44
+ end
45
+
46
+ it "symbolises any field keys" do
47
+ subject.put "example", { "template" => "static" }
48
+ YAML.load(File.read(File.join(content_path, "example.yml"))).should == { template: "static" }
49
+ end
50
+ end
51
+
52
+ context "when writing to a sub-directory" do
53
+ context "without an existing YAML file" do
54
+ it "creates the tree leading to the file" do
55
+ lambda { subject.put "sub_directory/example", { template: "static" } }.should create_file File.join(content_path, "sub_directory", "example.yml")
56
+ end
57
+ end
58
+
59
+ context "when a file in the path already exists" do
60
+ before(:each) { subject.put "sub_directory", { example: true } }
61
+
62
+ it "creates the tree to the file" do
63
+ lambda { subject.put "sub_directory/example", { template: "static" } }.should create_directory File.join(content_path, "sub_directory")
64
+ end
65
+
66
+ it "it moves the existing file into an index.yml file in the path" do
67
+ lambda { subject.put "sub_directory/example", { template: "static" } }.should move_file({
68
+ from: File.join(content_path, "sub_directory.yml"),
69
+ to: File.join(content_path, "sub_directory", "index.yml")
70
+ })
71
+ end
72
+ end
73
+
74
+ context "when the directory specified already exists" do
75
+ before(:each) { FileUtils.mkdir File.join(content_path, "sub_directory") }
76
+
77
+ it "creates the file as index.yml within the directory" do
78
+ lambda { subject.put "sub_directory", { template: "static" } }.should create_file File.join(content_path, "sub_directory", "index.yml")
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ describe "retrieving content from the repository" do
85
+ context "when the item does not exist" do
86
+ it "returns nil" do
87
+ subject.get("not_found").should be_nil
88
+ end
89
+ end
90
+
91
+ context "when the item exists" do
92
+ it "returns it's content in hash form" do
93
+ subject.put "example", { template: "static" }
94
+ subject.get("example").should == { template: "static" }
95
+ end
96
+
97
+ it "supports items with string based keys" do
98
+ File.open(File.join(content_path, "example.yml"), "w") { |f| f.print("template: foo") }
99
+ subject.get("example").should == { template: "foo" }
100
+ end
101
+ end
102
+
103
+ context "when the item is a directory" do
104
+ it "returns the contents of index.yml if it exists" do
105
+ subject.put "example/index", { template: "static" }
106
+ subject.get("example").should == { template: "static" }
107
+ end
108
+
109
+ it "returns nil if no index.yml exists" do
110
+ subject.put "example/page", { template: "static" }
111
+ subject.get("example").should be_nil
112
+ end
113
+ end
114
+ end
115
+ end