mascot 0.1.5 → 0.1.6

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: 5dd189be94137f4449cf5cd3adfe17cc3c73696d
4
- data.tar.gz: f7bd464af3cbcad19c4556a481d35fe6b09a4980
3
+ metadata.gz: 121fe032defb71fbc2265ccf6a0f850a91e2f47d
4
+ data.tar.gz: 3afef4d165793f1727c3dc8c43390419e6b2ceb3
5
5
  SHA512:
6
- metadata.gz: 3e3dd576d4168e4ac6b5bc1e54fe8bb0279fb1fbfc657831db743580608523fe481df1dd105a48069a02605a681aaafbb2466e3b5fb007456373530d0242b2cd
7
- data.tar.gz: ab69681818613b98b7f0cd4766bbdd86c55e6624812fbdf9783afb27491c42299088c0b61437d3099bf128efd39175fa0178c7fabb1f80314cd2f17a99aecaeb
6
+ metadata.gz: a674481e7d739c26f2517579808cc60494c64872865e45a496a7b8e144e2ecd053a11f4048c39d2d735bf413ebd5a118c4feda6c7d4ad4f57e640759c7cf17f9
7
+ data.tar.gz: 8a77fb0441652d75b55524207bbcbff4f11ebd780d646444318b1b7ba2c974a8a6c68207221aaa77d587e032327a5053f319d1449f2d733389b8f216cb1c0fde
@@ -0,0 +1,67 @@
1
+ require "mime/types"
2
+ require "forwardable"
3
+ require "pathname"
4
+
5
+ module Mascot
6
+ # Represents a file on a web server that may be parsed to extract
7
+ # frontmatter or be renderable via a template. Multiple resources
8
+ # may point to the same asset. Properties of an asset should be mutable.
9
+ # The Resource object is immutable and may be modified by the Resources proxy.
10
+ class Asset
11
+ # If we can't resolve a mime type for the resource, we'll fall
12
+ # back to this binary octet-stream type so the client can download
13
+ # the resource and figure out what to do with it.
14
+ DEFAULT_MIME_TYPE = MIME::Types["application/octet-stream"].first
15
+
16
+ attr_reader :path
17
+
18
+ extend Forwardable
19
+ def_delegators :frontmatter, :data, :body
20
+
21
+ def initialize(path: , mime_type: nil)
22
+ # The MIME::Types gem returns an array when types are looked up.
23
+ # This grabs the first one, which is likely the intent on these lookups.
24
+ @mime_type = Array(mime_type).first
25
+ @path = Pathname.new path
26
+ end
27
+
28
+ # List of all file extensions.
29
+ def extensions
30
+ path.basename.to_s.split(".").drop(1)
31
+ end
32
+
33
+ # Returns the format extension.
34
+ def format_extension
35
+ extensions.first
36
+ end
37
+
38
+ # Returns a list of the rendering extensions.
39
+ def template_extensions
40
+ extensions.drop(1)
41
+ end
42
+
43
+ # Treat resources with the same request path as equal.
44
+ def ==(asset)
45
+ path == asset.path
46
+ end
47
+
48
+ def mime_type
49
+ @mime_type ||= Array(inferred_mime_type).first || DEFAULT_MIME_TYPE
50
+ end
51
+
52
+ def exists?
53
+ File.exists? path
54
+ end
55
+
56
+ private
57
+ def frontmatter
58
+ Frontmatter.new File.read @path
59
+ end
60
+
61
+ # Returns the mime type of the file extension. If a type can't
62
+ # be resolved then we'll just grab the first type.
63
+ def inferred_mime_type
64
+ MIME::Types.type_for(format_extension) if format_extension
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,19 @@
1
+ require "yaml"
2
+
3
+ module Mascot
4
+ # Parses metadata from the header of the page.
5
+ class Frontmatter
6
+ DELIMITER = "---".freeze
7
+ PATTERN = /\A(#{DELIMITER}\n(.+)\n#{DELIMITER}\n)?(.+)\Z/m
8
+
9
+ attr_reader :body
10
+
11
+ def initialize(content)
12
+ _, @data, @body = content.match(PATTERN).captures
13
+ end
14
+
15
+ def data
16
+ @data ? YAML.load(@data) : {}
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,56 @@
1
+ require "forwardable"
2
+ require "observer"
3
+
4
+ module Mascot
5
+ # Represents the request path of an asset. There may be multiple
6
+ # resources that point to the same asset. Resources are immutable
7
+ # and may be altered by the resource proxy.
8
+ class Resource
9
+ include Observable
10
+
11
+ extend Forwardable
12
+ def_delegators :asset, :mime_type
13
+
14
+ attr_accessor :request_path, :asset
15
+ attr_writer :body, :data
16
+
17
+ def initialize(request_path: , asset: )
18
+ self.request_path = request_path
19
+ @asset = asset
20
+ end
21
+
22
+ # When #dup or #clone is copied, the Resource
23
+ # collection observer must be removed so there's
24
+ # not duplicate resources.
25
+ def initialize_copy(instance)
26
+ instance.delete_observers
27
+ super instance
28
+ end
29
+
30
+ def request_path=(request_path)
31
+ old_request_path = @request_path
32
+ # We freeze the value to ensure users can't modify
33
+ # the request_path string in place (e.g. Resource#request_path.capitalize!)
34
+ # and throw the resource out of sync with the Resources collection.
35
+ @request_path = request_path.dup.freeze
36
+ changed
37
+ notify_observers self, old_request_path
38
+ end
39
+
40
+ def inspect
41
+ "#<#{self.class}:0x#{(object_id << 1).to_s(16)} @request_path=#{@request_path.inspect} @asset=#{@asset.inspect}>"
42
+ end
43
+
44
+ def data
45
+ @data ||= asset.data
46
+ end
47
+
48
+ def body
49
+ @body ||= asset.body
50
+ end
51
+
52
+ def ==(asset)
53
+ request_path == asset.request_path
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,97 @@
1
+ module Mascot
2
+ class Resources
3
+ include Enumerable
4
+
5
+ extend Forwardable
6
+ def_delegators :@routes, :size, :empty?, :any?, :clear
7
+
8
+ def initialize(root_file_path: )
9
+ @routes = Hash.new
10
+ @root_file_path = Pathname.new(root_file_path)
11
+ end
12
+
13
+ def each(&block)
14
+ @routes.values.each(&block)
15
+ end
16
+
17
+ def last
18
+ @routes.values.last
19
+ end
20
+
21
+ def glob(pattern = "**/**")
22
+ paths = safe_root.glob @root_file_path.join(pattern)
23
+ select { |r| paths.include? r.asset.path.to_s}
24
+ end
25
+
26
+ def get(request_path)
27
+ return if request_path.nil?
28
+ @routes[key(request_path)]
29
+ end
30
+
31
+ def add(resource)
32
+ validate_request_path resource
33
+ validate_uniqueness resource
34
+
35
+ resource.add_observer self
36
+ @routes[key(resource)] = resource
37
+ end
38
+
39
+ def update(resource, old_request_path)
40
+ validate_request_path old_request_path
41
+ validate_request_path resource
42
+ validate_uniqueness resource
43
+
44
+ @routes.delete key(old_request_path)
45
+ @routes[key(resource)] = resource
46
+ end
47
+
48
+ def remove(resource)
49
+ validate_request_path resource
50
+ resource.delete_observer self
51
+ @routes.delete key(resource)
52
+ resource
53
+ end
54
+
55
+ def add_asset(asset, request_path: nil)
56
+ add Resource.new asset: asset, request_path: asset_path_to_request_path(request_path || asset.path)
57
+ end
58
+
59
+ private
60
+ def key(path)
61
+ File.join "/", validate_request_path(coerce_request_path(path))
62
+ end
63
+
64
+ def coerce_request_path(resource)
65
+ resource.respond_to?(:request_path) ? resource.request_path : resource
66
+ end
67
+
68
+ def validate_request_path(path)
69
+ path = coerce_request_path(path)
70
+ raise InvalidRequestPathError, "path can't be nil" if path.nil?
71
+ path
72
+ end
73
+
74
+ # Raise an exception if the user tries to add a Resource with an existing request path.
75
+ def validate_uniqueness(resource)
76
+ path = coerce_request_path(resource)
77
+ if existing_resource = get(path)
78
+ raise ExistingRequestPathError, "Resource #{existing_resource} already exists at #{path}"
79
+ else
80
+ resource
81
+ end
82
+ end
83
+
84
+ # Given a @file_path of `/hi`, this method changes `/hi/there/friend.html.erb`
85
+ # to an absolute `/there/friend` format by removing the file extensions
86
+ def asset_path_to_request_path(path)
87
+ # Relative path of resource to the file_path of this project.
88
+ relative_path = Pathname.new(path).relative_path_from(@root_file_path)
89
+ # Removes the .fooz.baz
90
+ File.join("/", relative_path).to_s.sub(/\..*/, '')
91
+ end
92
+
93
+ def safe_root
94
+ @safe_root ||= SafeRoot.new(path: @root_file_path)
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,42 @@
1
+ require "pathname"
2
+
3
+ module Mascot
4
+ # Validates if a path is within another path. This prevents
5
+ # users from accidentally selecting a file outside of their sitemap,
6
+ # which could be insured.
7
+ class SafeRoot
8
+ def initialize(path: )
9
+ @path = Pathname.new(path)
10
+ end
11
+
12
+ # Validates if a path is safe by checking if its within a folder.
13
+ def safe?(path)
14
+ root_path = File.expand_path(@path)
15
+ resource_path = File.expand_path(path)
16
+
17
+ if resource_path.start_with? root_path
18
+ path
19
+ else
20
+ end
21
+ end
22
+
23
+ def glob(pattern)
24
+ Dir[validate(pattern)]
25
+ end
26
+
27
+ def unsafe?(path)
28
+ not safe? path
29
+ end
30
+
31
+ def path
32
+ end
33
+
34
+ def validate(path)
35
+ if unsafe? path
36
+ raise Mascot::UnsafePathAccessError, "Unsafe attempt to access #{path} outside of #{@path}"
37
+ else
38
+ path
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,52 @@
1
+ require "pathname"
2
+
3
+ module Mascot
4
+ # A collection of pages from a directory.
5
+ class Sitemap
6
+ # Default file pattern to pick up in sitemap
7
+ DEFAULT_GLOB = "**/**".freeze
8
+ # Default root path for sitemap.
9
+ DEFAULT_ROOT_PATH = Pathname.new(".").freeze
10
+ # Default root request path
11
+ DEFAULT_ROOT_REQUEST_PATH = Pathname.new("/").freeze
12
+
13
+ attr_reader :root, :request_path
14
+
15
+ def initialize(root: DEFAULT_ROOT_PATH, request_path: DEFAULT_ROOT_REQUEST_PATH)
16
+ self.root = root
17
+ self.request_path = request_path
18
+ end
19
+
20
+ # Lazy stream of files that will be rendered by resources.
21
+ def assets(glob = DEFAULT_GLOB)
22
+ safe_root.glob(root.join(glob)).select(&File.method(:file?)).lazy.map do |path|
23
+ Asset.new(path: path)
24
+ end
25
+ end
26
+
27
+ # Returns a list of resources.
28
+ def resources
29
+ Resources.new(root_file_path: root).tap do |resources|
30
+ assets.each { |a| resources.add_asset a }
31
+ end
32
+ end
33
+
34
+ # Find the page with a path.
35
+ def get(request_path)
36
+ resources.get(request_path)
37
+ end
38
+
39
+ def root=(path)
40
+ @root = Pathname.new(path)
41
+ end
42
+
43
+ def request_path=(path)
44
+ @request_path = Pathname.new(path)
45
+ end
46
+
47
+ private
48
+ def safe_root
49
+ @safe_root ||= SafeRoot.new(path: root)
50
+ end
51
+ end
52
+ end
@@ -1,3 +1,3 @@
1
1
  module Mascot
2
- VERSION = "0.1.5"
2
+ VERSION = "0.1.6"
3
3
  end
data/lib/mascot.rb CHANGED
@@ -1,145 +1,19 @@
1
1
  require "mascot/version"
2
2
 
3
- require "forwardable"
4
- require "pathname"
5
- require "yaml"
6
- require "mime/types"
7
-
8
3
  module Mascot
9
4
  # Raised if a user attempts to access a resource outside of the sitemap path.
10
- InsecurePathAccessError = Class.new(SecurityError)
11
-
12
- # Parses metadata from the header of the page.
13
- class Frontmatter
14
- DELIMITER = "---".freeze
15
- PATTERN = /\A(#{DELIMITER}\n(.+)\n#{DELIMITER}\n)?(.+)\Z/m
16
-
17
- attr_reader :body
18
-
19
- def initialize(content)
20
- _, @data, @body = content.match(PATTERN).captures
21
- end
22
-
23
- def data
24
- @data ? YAML.load(@data) : {}
25
- end
26
-
27
- private
28
- def parse
29
- @content
30
- end
31
- end
32
-
33
- # Represents a page in a web server context.
34
- class Resource
35
- # If we can't resolve a mime type for the resource, we'll fall
36
- # back to this binary octet-stream type so the client can download
37
- # the resource and figure out what to do with it.
38
- DEFAULT_MIME_TYPE = MIME::Types["application/octet-stream"].first
39
-
40
- attr_reader :request_path, :file_path
41
-
42
- extend Forwardable
43
- def_delegators :@frontmatter, :data, :body
44
-
45
- def initialize(request_path: , file_path: , mime_type: nil)
46
- @request_path = request_path
47
- @file_path = Pathname.new file_path
48
- @frontmatter = Frontmatter.new File.read @file_path
49
- @mime_types = Array(mime_type) if mime_type
50
- end
51
-
52
- # List of all file extensions.
53
- def extensions
54
- @file_path.basename.to_s.split(".").drop(1)
55
- end
56
-
57
- # Returns the format extension.
58
- def format_extension
59
- extensions.first
60
- end
61
-
62
- # Returns a list of the rendering extensions.
63
- def template_extensions
64
- extensions.drop(1)
65
- end
66
-
67
- def mime_type
68
- (@mime_types ||= Array(resolve_mime_type)).push(DEFAULT_MIME_TYPE).first
69
- end
70
-
71
- # Treat resources with the same request path as equal.
72
- def ==(resource)
73
- request_path == resource.request_path
74
- end
75
-
76
- private
77
- # Returns the mime type of the file extension. If a type can't
78
- # be resolved then we'll just grab the first type.
79
- def resolve_mime_type
80
- MIME::Types.type_for(format_extension) if format_extension
81
- end
82
- end
83
-
84
- # A collection of pages from a directory.
85
- class Sitemap
86
- # Default file pattern to pick up in sitemap
87
- DEFAULT_GLOB = "**/**".freeze
88
- # Default root path for sitemap.
89
- DEFAULT_ROOT_DIR = Pathname.new(".").freeze
90
- # Default root request path
91
- DEFAULT_ROOT_REQUEST_PATH = Pathname.new("/").freeze
92
-
93
- attr_reader :file_path, :request_path
94
-
95
- def initialize(file_path: DEFAULT_ROOT_DIR, request_path: DEFAULT_ROOT_REQUEST_PATH)
96
- self.file_path = file_path
97
- self.request_path = request_path
98
- end
99
-
100
- # Lazy stream of resources.
101
- def resources(glob = DEFAULT_GLOB)
102
- Dir[validate_path(@file_path.join(glob))].select(&File.method(:file?)).lazy.map do |path|
103
- Resource.new request_path: request_path(path), file_path: path
104
- end
105
- end
106
-
107
- # Find the page with a path.
108
- def find_by_request_path(request_path)
109
- return if request_path.nil?
110
- resources.find { |r| r.request_path == File.join("/", request_path) }
111
- end
112
-
113
- def file_path=(path)
114
- @file_path = Pathname.new(path)
115
- end
116
-
117
- def request_path=(path)
118
- @request_path = Pathname.new(path)
119
- end
120
-
121
- private
5
+ UnsafePathAccessError = Class.new(SecurityError)
122
6
 
123
- # Make sure the user is accessing a file within the root path of the
124
- # sitemap.
125
- def validate_path(path)
126
- root_path = @file_path.expand_path.to_s
127
- resource_path = path.expand_path.to_s
7
+ # Raised by Resources if a path is added that's not a valid path.
8
+ InvalidRequestPathError = Class.new(RuntimeError)
128
9
 
129
- if resource_path.start_with? root_path
130
- path
131
- else
132
- raise Mascot::InsecurePathAccessError, "#{resource_path} outside sitemap #{root_path} directory"
133
- end
134
- end
10
+ # Raised by Resources if a path is already in its index
11
+ ExistingRequestPathError = Class.new(InvalidRequestPathError)
135
12
 
136
- # Given a @file_path of `/hi`, this method changes `/hi/there/friend.html.erb`
137
- # to an absolute `/there/friend` format by removing the file extensions
138
- def request_path(path)
139
- # Relative path of resource to the file_path of this project.
140
- relative_path = Pathname.new(path).relative_path_from(@file_path)
141
- # Removes the .fooz.baz
142
- @request_path.join(relative_path).to_s.sub(/\..*/, '')
143
- end
144
- end
13
+ autoload :Asset, "mascot/asset"
14
+ autoload :Frontmatter, "mascot/frontmatter"
15
+ autoload :SafeRoot, "mascot/safe_root"
16
+ autoload :Resources, "mascot/resources"
17
+ autoload :Resource, "mascot/resource"
18
+ autoload :Sitemap, "mascot/sitemap"
145
19
  end
data/mascot.gemspec CHANGED
@@ -20,7 +20,6 @@ Gem::Specification.new do |spec|
20
20
  spec.add_development_dependency "bundler", "~> 1.11"
21
21
  spec.add_development_dependency "rake", "~> 10.0"
22
22
  spec.add_development_dependency "rspec", "~> 3.0"
23
- spec.add_development_dependency "pry"
24
23
 
25
24
  spec.add_runtime_dependency "mime-types", ">= 2.99"
26
25
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mascot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brad Gessler
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-07-24 00:00:00.000000000 Z
11
+ date: 2016-07-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -52,20 +52,6 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '3.0'
55
- - !ruby/object:Gem::Dependency
56
- name: pry
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: '0'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: '0'
69
55
  - !ruby/object:Gem::Dependency
70
56
  name: mime-types
71
57
  requirement: !ruby/object:Gem::Requirement
@@ -88,6 +74,12 @@ extensions: []
88
74
  extra_rdoc_files: []
89
75
  files:
90
76
  - lib/mascot.rb
77
+ - lib/mascot/asset.rb
78
+ - lib/mascot/frontmatter.rb
79
+ - lib/mascot/resource.rb
80
+ - lib/mascot/resources.rb
81
+ - lib/mascot/safe_root.rb
82
+ - lib/mascot/sitemap.rb
91
83
  - lib/mascot/version.rb
92
84
  - mascot.gemspec
93
85
  homepage: https://github.com/bradgessler/mascot