mascot 0.1.5 → 0.1.6

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: 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