landline 0.9.2
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 +7 -0
- data/HACKING.md +30 -0
- data/LAYOUT.md +59 -0
- data/LICENSE.md +660 -0
- data/README.md +159 -0
- data/lib/landline/dsl/constructors_path.rb +107 -0
- data/lib/landline/dsl/constructors_probe.rb +28 -0
- data/lib/landline/dsl/methods_common.rb +28 -0
- data/lib/landline/dsl/methods_path.rb +75 -0
- data/lib/landline/dsl/methods_probe.rb +129 -0
- data/lib/landline/dsl/methods_template.rb +16 -0
- data/lib/landline/node.rb +87 -0
- data/lib/landline/path.rb +157 -0
- data/lib/landline/pattern_matching/glob.rb +168 -0
- data/lib/landline/pattern_matching/rematch.rb +49 -0
- data/lib/landline/pattern_matching/util.rb +15 -0
- data/lib/landline/pattern_matching.rb +75 -0
- data/lib/landline/probe/handler.rb +56 -0
- data/lib/landline/probe/http_method.rb +74 -0
- data/lib/landline/probe/serve_handler.rb +39 -0
- data/lib/landline/probe.rb +62 -0
- data/lib/landline/request.rb +135 -0
- data/lib/landline/response.rb +140 -0
- data/lib/landline/server.rb +49 -0
- data/lib/landline/template/erb.rb +27 -0
- data/lib/landline/template/erubi.rb +36 -0
- data/lib/landline/template.rb +95 -0
- data/lib/landline/util/cookie.rb +150 -0
- data/lib/landline/util/errors.rb +11 -0
- data/lib/landline/util/html.rb +119 -0
- data/lib/landline/util/lookup.rb +37 -0
- data/lib/landline/util/mime.rb +1276 -0
- data/lib/landline/util/multipart.rb +175 -0
- data/lib/landline/util/parsesorting.rb +37 -0
- data/lib/landline/util/parseutils.rb +111 -0
- data/lib/landline/util/query.rb +66 -0
- data/lib/landline.rb +20 -0
- metadata +85 -0
data/README.md
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
# Landline - an HTTP DSL
|
2
|
+
|
3
|
+
Landline is a library that provides a minimalistic DSL for creating
|
4
|
+
web services. It doesn't include patterns, middleware, or anything that
|
5
|
+
could be considered application logic. It does a few things, and hopefully
|
6
|
+
it does them well:
|
7
|
+
|
8
|
+
- Routing HTTP requests to handlers
|
9
|
+
- Processing HTTP requests (cookies, headers, etc.)
|
10
|
+
- Filtering, preprocessing and postprocessing requests
|
11
|
+
- Creating responses from templates using various template engines
|
12
|
+
- Parsing and handling forms and queries
|
13
|
+
- Connecting multiple Landline applications together
|
14
|
+
|
15
|
+
As such, the library is pretty thin and can be used to build more complex
|
16
|
+
applications.
|
17
|
+
|
18
|
+
As of now it is using Rack as the webserver adapter, but ideally it
|
19
|
+
shouldn't take much work to make it run on top of any webserver.
|
20
|
+
|
21
|
+
Landline was made mostly for fun. Ideally it will become something more,
|
22
|
+
but as of yet it's just an experiment revolving around Ruby Metaprogramming
|
23
|
+
and its DSL capabilities.
|
24
|
+
|
25
|
+
## Examples
|
26
|
+
|
27
|
+
A simple "Hello, World!" app using Landline
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
require 'landline'
|
31
|
+
|
32
|
+
app = Landline::Server.new do
|
33
|
+
get "/hello" do
|
34
|
+
header "content-type", "text/plain"
|
35
|
+
"Hello world!"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
run app
|
40
|
+
```
|
41
|
+
|
42
|
+
A push/pull stack as an app
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
require 'landline'
|
46
|
+
|
47
|
+
stack = []
|
48
|
+
|
49
|
+
app = Landline::Server.new do
|
50
|
+
get "/pop" do
|
51
|
+
header 'content-type', 'text/plain'
|
52
|
+
stack.pop.to_s
|
53
|
+
end
|
54
|
+
post "/push" do
|
55
|
+
header 'content-type', 'text/plain'
|
56
|
+
stack.push(request.body)
|
57
|
+
request.body
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
run app
|
62
|
+
```
|
63
|
+
|
64
|
+
Several push/pull buckets
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
require 'landline'
|
68
|
+
|
69
|
+
stack = { "1" => [], "2" => [], "3" => [] }
|
70
|
+
|
71
|
+
app = Landline::Server.new do
|
72
|
+
path "bucket_(1|2|3)" do
|
73
|
+
get "pop" do |bucket|
|
74
|
+
header "content-type", "text/plain"
|
75
|
+
stack[bucket].pop.to_s
|
76
|
+
end
|
77
|
+
post "push" do |bucket|
|
78
|
+
header "content-type", "text/plain"
|
79
|
+
stack[bucket].push(request.body)
|
80
|
+
request.body
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
run app
|
86
|
+
```
|
87
|
+
|
88
|
+
Static file serving
|
89
|
+
(Note: index applies *only* to /var/www (to the path its defined in))
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
require 'landline'
|
93
|
+
|
94
|
+
app = Landline::Server.new do
|
95
|
+
root "/var/www"
|
96
|
+
index ["index.html","index.htm"]
|
97
|
+
serve "**/*.(html|htm)"
|
98
|
+
end
|
99
|
+
|
100
|
+
run app
|
101
|
+
```
|
102
|
+
|
103
|
+
Logging on a particular path
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
require 'landline'
|
107
|
+
|
108
|
+
app = Landline::Server.new do
|
109
|
+
path "unimportant" do
|
110
|
+
get "version" do
|
111
|
+
header "content-type", "text/plain"
|
112
|
+
"1337 (the best one)"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
path "important" do
|
116
|
+
preprocess do |req|
|
117
|
+
# Implement logging logic here
|
118
|
+
puts "Client at #{req.headers['remote-addr']} wanted to access something /important!"
|
119
|
+
end
|
120
|
+
get "answer" do
|
121
|
+
header "content-type", "application/json"
|
122
|
+
'{"answer":42, "desc":"something important!"}'
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
run app
|
128
|
+
```
|
129
|
+
|
130
|
+
And a lot more to be found in /examples in this repo.
|
131
|
+
|
132
|
+
## Name
|
133
|
+
|
134
|
+
The name is, quite literally, a metaphor for request routing.
|
135
|
+
|
136
|
+
## Documentation
|
137
|
+
|
138
|
+
Documentation can be generated using `yard doc`.
|
139
|
+
For things to render correctly, please install the `redcarpet` gem.
|
140
|
+
|
141
|
+
## License
|
142
|
+
|
143
|
+
```plain
|
144
|
+
Landline - an HTTP request pattern matching system
|
145
|
+
Copyright (C) 2022 yessiest (yessiest@memeware.net)
|
146
|
+
|
147
|
+
This program is free software: you can redistribute it and/or modify
|
148
|
+
it under the terms of the GNU General Public License as published by
|
149
|
+
the Free Software Foundation, either version 3 of the License, or
|
150
|
+
(at your option) any later version.
|
151
|
+
|
152
|
+
This program is distributed in the hope that it will be useful,
|
153
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
154
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
155
|
+
GNU General Public License for more details.
|
156
|
+
|
157
|
+
You should have received a copy of the GNU General Public License
|
158
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
159
|
+
```
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Landline
|
4
|
+
# Shared DSL methods
|
5
|
+
module DSL
|
6
|
+
# Path (and subclasses) DSL constructors
|
7
|
+
module PathConstructors
|
8
|
+
# Append a Node child object to the list of children
|
9
|
+
def register(obj)
|
10
|
+
unless obj.is_a? Landline::Node
|
11
|
+
raise ArgumentError, "register accepts node children only"
|
12
|
+
end
|
13
|
+
|
14
|
+
@origin.children.append(obj)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Create a new {Landline::Path} object
|
18
|
+
def path(path, **args, &setup)
|
19
|
+
register(Landline::Path.new(path, parent: @origin, **args, &setup))
|
20
|
+
end
|
21
|
+
|
22
|
+
# Create a new {Landline::Handlers::Probe} object
|
23
|
+
def probe(path, **args, &_setup)
|
24
|
+
register(Landline::Handlers::Probe.new(path,
|
25
|
+
parent: @origin,
|
26
|
+
**args))
|
27
|
+
end
|
28
|
+
|
29
|
+
# Create a new {Landline::Handlers::GETHandler} object
|
30
|
+
def get(path, **args, &setup)
|
31
|
+
register(Landline::Handlers::GET.new(path,
|
32
|
+
parent: @origin,
|
33
|
+
**args,
|
34
|
+
&setup))
|
35
|
+
end
|
36
|
+
|
37
|
+
# create a new {Landline::Handlers::POSTHandler} object
|
38
|
+
def post(path, **args, &setup)
|
39
|
+
register(Landline::Handlers::POST.new(path,
|
40
|
+
parent: @origin,
|
41
|
+
**args,
|
42
|
+
&setup))
|
43
|
+
end
|
44
|
+
|
45
|
+
# Create a new {Landline::Handlers::PUTHandler} object
|
46
|
+
def put(path, **args, &setup)
|
47
|
+
register(Landline::Handlers::PUT.new(path,
|
48
|
+
parent: @origin,
|
49
|
+
**args,
|
50
|
+
&setup))
|
51
|
+
end
|
52
|
+
|
53
|
+
# Create a new {Landline::Handlers::HEADHandler} object
|
54
|
+
def head(path, **args, &setup)
|
55
|
+
register(Landline::Handlers::HEAD.new(path,
|
56
|
+
parent: @origin,
|
57
|
+
**args,
|
58
|
+
&setup))
|
59
|
+
end
|
60
|
+
|
61
|
+
# Create a new {Landline::Handlers::DELETEHandler} object
|
62
|
+
def delete(path, **args, &setup)
|
63
|
+
register(Landline::Handlers::DELETE.new(path,
|
64
|
+
parent: @origin,
|
65
|
+
**args,
|
66
|
+
&setup))
|
67
|
+
end
|
68
|
+
|
69
|
+
# Create a new {Landline::Handlers::CONNECTHandler} object
|
70
|
+
def connect(path, **args, &setup)
|
71
|
+
register(Landline::Handlers::CONNECT.new(path,
|
72
|
+
parent: @origin,
|
73
|
+
**args,
|
74
|
+
&setup))
|
75
|
+
end
|
76
|
+
|
77
|
+
# Create a new {Landline::Handlers::TRACEHandler} object
|
78
|
+
def trace(path, **args, &setup)
|
79
|
+
register(Landline::Handlers::TRACE.new(path,
|
80
|
+
parent: @origin,
|
81
|
+
**args,
|
82
|
+
&setup))
|
83
|
+
end
|
84
|
+
|
85
|
+
# Create a new {Landline::Handlers::PATCHHandler} object
|
86
|
+
def patch(path, **args, &setup)
|
87
|
+
register(Landline::Handlers::PATCH.new(path,
|
88
|
+
parent: @origin,
|
89
|
+
**args,
|
90
|
+
&setup))
|
91
|
+
end
|
92
|
+
|
93
|
+
# Create a new {Landline::Handlers::OPTIONSHandler} object
|
94
|
+
def options(path, **args, &setup)
|
95
|
+
register(Landline::Handlers::OPTIONS.new(path,
|
96
|
+
parent: @origin,
|
97
|
+
**args,
|
98
|
+
&setup))
|
99
|
+
end
|
100
|
+
|
101
|
+
# Create a new {Landline::Handlers::GETHandler} that serves static files
|
102
|
+
def serve(path)
|
103
|
+
register(Landline::Handlers::Serve.new(path, parent: @origin))
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Landline
|
4
|
+
module DSL
|
5
|
+
# Probe (and subclasses) DSL construct
|
6
|
+
module ProbeConstructors
|
7
|
+
# Create a new erb template
|
8
|
+
# @see {Landline::Template#new}
|
9
|
+
def erb(input, vars = {})
|
10
|
+
Landline::Templates::ERB.new(input,
|
11
|
+
vars,
|
12
|
+
parent: @origin)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Create a new erb template using Erubi engine
|
16
|
+
# @see {Landline::Template#new}
|
17
|
+
# @param freeze [Boolean] whether to use frozen string literal
|
18
|
+
# @param capture [Boolean] whether to enable output capturing
|
19
|
+
def erubi(input, vars = {}, freeze: true, capture: false)
|
20
|
+
Landline::Templates::Erubi.new(input,
|
21
|
+
vars,
|
22
|
+
parent: @origin,
|
23
|
+
freeze: freeze,
|
24
|
+
capture: capture)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Landline
|
4
|
+
module DSL
|
5
|
+
# Methods shared by probes, preprocessors and filters.
|
6
|
+
module CommonMethods
|
7
|
+
# Stop execution and generate a boilerplate response with the given code
|
8
|
+
# @param errorcode [Integer]
|
9
|
+
# @param backtrace [Array(String), nil]
|
10
|
+
# @raise [UncaughtThrowError] throws :finish to return back to Server
|
11
|
+
def die(errorcode, backtrace: nil)
|
12
|
+
throw :finish, [errorcode].append(
|
13
|
+
*(@origin.properties["handle.#{errorcode}"] or
|
14
|
+
@origin.properties["handle.default"]).call(
|
15
|
+
errorcode,
|
16
|
+
backtrace: backtrace
|
17
|
+
)
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Bounce request to the next handler
|
22
|
+
# @raise [UncaughtThrowError] throws :break to get out of the callback
|
23
|
+
def bounce
|
24
|
+
throw :break
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Landline
|
4
|
+
# Shared DSL methods
|
5
|
+
module DSL
|
6
|
+
# Common path methods
|
7
|
+
module PathMethods
|
8
|
+
# Set path index
|
9
|
+
# @param index [Array,String]
|
10
|
+
def index(index)
|
11
|
+
case index
|
12
|
+
when Array
|
13
|
+
@origin.properties['index'] = index
|
14
|
+
when String
|
15
|
+
@origin.properties['index'] = [index]
|
16
|
+
else
|
17
|
+
raise ArgumentError, "index should be an Array or a String"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Set root path (appends matched part of the path).
|
22
|
+
# @param path [String]
|
23
|
+
def root(path)
|
24
|
+
@origin.root = path
|
25
|
+
end
|
26
|
+
|
27
|
+
# Set root path (without appending matched part).
|
28
|
+
# @param path [String]
|
29
|
+
def remap(path)
|
30
|
+
@origin.remap = path
|
31
|
+
end
|
32
|
+
|
33
|
+
# Add a preprocessor to the path.
|
34
|
+
# Does not modify path execution.
|
35
|
+
# @param block [#call]
|
36
|
+
# @yieldparam request [Landline::Request]
|
37
|
+
def preprocess(&block)
|
38
|
+
@origin.preprocess(&block)
|
39
|
+
block
|
40
|
+
end
|
41
|
+
|
42
|
+
# Add a postprocessor to the path.
|
43
|
+
# @param block [#call]
|
44
|
+
# @yieldparam request [Landline::Request]
|
45
|
+
# @yieldparam response [Landline::Response]
|
46
|
+
def postprocess(&block)
|
47
|
+
@origin.postprocess(&block)
|
48
|
+
block
|
49
|
+
end
|
50
|
+
|
51
|
+
# Add a filter to the path.
|
52
|
+
# Blocks path access if a filter returns false.
|
53
|
+
# @param block [#call]
|
54
|
+
# @yieldparam request [Landline::Request]
|
55
|
+
def filter(&block)
|
56
|
+
@origin.filter(&block)
|
57
|
+
block
|
58
|
+
end
|
59
|
+
|
60
|
+
# Include an application as a child of path.
|
61
|
+
# @param filename [String]
|
62
|
+
def plugin(filename)
|
63
|
+
self.define_singleton_method(:run) do |object|
|
64
|
+
unless object.is_a? Landline::Node
|
65
|
+
raise ArgumentError, "not a node instance or subclass instance"
|
66
|
+
end
|
67
|
+
|
68
|
+
object
|
69
|
+
end
|
70
|
+
@origin.children.append(self.instance_eval(File.read(filename)))
|
71
|
+
self.singleton_class.undef_method :run
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../response'
|
4
|
+
require_relative '../util/multipart'
|
5
|
+
require_relative '../util/parseutils'
|
6
|
+
require_relative '../util/html'
|
7
|
+
|
8
|
+
module Landline
|
9
|
+
module DSL
|
10
|
+
# Common methods for Probe objects
|
11
|
+
module ProbeMethods
|
12
|
+
# Get the current request
|
13
|
+
# @return [Landline::Request]
|
14
|
+
def request
|
15
|
+
@origin.request
|
16
|
+
end
|
17
|
+
|
18
|
+
# Set response status (generate response if one doesn't exist yet)
|
19
|
+
# @param status [Integer] http status code
|
20
|
+
def status(status)
|
21
|
+
@origin.response = (@origin.response or Landline::Response.new)
|
22
|
+
@origin.response.status = status
|
23
|
+
end
|
24
|
+
|
25
|
+
alias code status
|
26
|
+
|
27
|
+
# Set response header (generate response if one doesn't exist yet)
|
28
|
+
# @param key [String] header name
|
29
|
+
# @param value [String] header value
|
30
|
+
def header(key, value)
|
31
|
+
return status(value) if key.downcase == "status"
|
32
|
+
|
33
|
+
unless key.match(Landline::Util::HeaderRegexp::TOKEN)
|
34
|
+
raise ArgumentError, "header key has invalid characters"
|
35
|
+
end
|
36
|
+
|
37
|
+
unless value&.match(Landline::Util::HeaderRegexp::PRINTABLE)
|
38
|
+
raise ArgumentError, "value key has invalid characters"
|
39
|
+
end
|
40
|
+
|
41
|
+
@origin.response = (@origin.response or Landline::Response.new)
|
42
|
+
key = key.downcase
|
43
|
+
@origin.response.add_header(key, value)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Delete a header value from the headers hash
|
47
|
+
# If no value is provided, deletes all key entries
|
48
|
+
# @param key [String] header name
|
49
|
+
# @param value [String, nil] header value
|
50
|
+
def delete_header(key, value = nil)
|
51
|
+
return unless @origin.response
|
52
|
+
|
53
|
+
return if key.downcase == "status"
|
54
|
+
|
55
|
+
unless key.match(Landline::Util::HeaderRegexp::TOKEN)
|
56
|
+
raise ArgumentError, "header key has invalid characters"
|
57
|
+
end
|
58
|
+
|
59
|
+
unless value&.match(Landline::Util::HeaderRegexp::PRINTABLE)
|
60
|
+
raise ArgumentError, "value key has invalid characters"
|
61
|
+
end
|
62
|
+
|
63
|
+
@origin.response.delete_header(key, value)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Set response cookie
|
67
|
+
# @see Landline::Cookie.new
|
68
|
+
def cookie(*params, **options)
|
69
|
+
@origin.response = (@origin.response or Landline::Response.new)
|
70
|
+
@origin.response.add_cookie(
|
71
|
+
Landline::Cookie.new(*params, **options)
|
72
|
+
)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Delete a cookie
|
76
|
+
# If no value is provided, deletes all cookies with the same key
|
77
|
+
# @param key [String] cookie key
|
78
|
+
# @param value [String, nil] cookie.value
|
79
|
+
def delete_cookie(key, value = nil)
|
80
|
+
return unless @origin.response
|
81
|
+
|
82
|
+
@origin.response.delete_cookie(key, value)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Checks if current request has multipart/form-data associated with it
|
86
|
+
# @return [Boolean]
|
87
|
+
def form?
|
88
|
+
value, opts = Landline::Util::ParserCommon.parse_value(
|
89
|
+
request.headers["content-type"]
|
90
|
+
)
|
91
|
+
if value == "multipart/form-data" and
|
92
|
+
opts["boundary"]
|
93
|
+
true
|
94
|
+
else
|
95
|
+
false
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Returns formdata
|
100
|
+
# @return [Hash{String=>(String,Landline::Util::FormPart)}]
|
101
|
+
def form
|
102
|
+
_, opts = Landline::Util::ParserCommon.parse_value(
|
103
|
+
request.headers["content-type"]
|
104
|
+
)
|
105
|
+
Landline::Util::MultipartParser.new(
|
106
|
+
request.input, opts["boundary"]
|
107
|
+
).to_h
|
108
|
+
end
|
109
|
+
|
110
|
+
# Open a file relative to current filepath
|
111
|
+
# @see File.open
|
112
|
+
def file(path, mode = "r", *all, &block)
|
113
|
+
File.open("#{request.filepath}/#{path}", mode, *all, &block)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Escape HTML entities
|
117
|
+
# @see Landline::Util.escape_html
|
118
|
+
def escape_html(text)
|
119
|
+
Landline::Util.escape_html(text)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Unescape HTML entities
|
123
|
+
# @see Landline::Util.escape_html
|
124
|
+
def unescape_html(text)
|
125
|
+
Landline::Util.unescape_html(text)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'erb'
|
4
|
+
require 'securerandom'
|
5
|
+
|
6
|
+
module Landline
|
7
|
+
module DSL
|
8
|
+
# Common methods for template contexts
|
9
|
+
module TemplateMethods
|
10
|
+
# Import a template part
|
11
|
+
def import(filepath)
|
12
|
+
@parent_template.import(file(filepath)).run
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'pattern_matching/util'
|
4
|
+
|
5
|
+
module Landline
|
6
|
+
# Abstract class that reacts to request navigation.
|
7
|
+
# Does nothing by default, behaviour should be overriden through
|
8
|
+
# #reject and #process
|
9
|
+
# @abstract
|
10
|
+
class Node
|
11
|
+
# @param path [Object]
|
12
|
+
# @param filepath [Boolean] should Node modify request.filepath.
|
13
|
+
def initialize(path, parent:, filepath: true)
|
14
|
+
@pattern = Pattern.new(path).freeze
|
15
|
+
@properties = Landline::Util::Lookup.new(parent&.properties)
|
16
|
+
@root = nil
|
17
|
+
@remap = false
|
18
|
+
@modify_filepath = filepath
|
19
|
+
end
|
20
|
+
|
21
|
+
# Set Node file root (like root in Nginx)
|
22
|
+
# @param path [String]
|
23
|
+
def root=(path)
|
24
|
+
raise ArgumentError, "path should be a String" unless path.is_a? String
|
25
|
+
|
26
|
+
@properties["path"] = File.expand_path(path)
|
27
|
+
@root = File.expand_path(path)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Set Node absolute file path (like alias in Nginx)
|
31
|
+
# @param path [String]
|
32
|
+
def remap=(path)
|
33
|
+
self.root = path
|
34
|
+
@remap = true
|
35
|
+
end
|
36
|
+
|
37
|
+
# Try to navigate the path. Run method callback in response.
|
38
|
+
# @param [Landline::Request]
|
39
|
+
# @return [Boolean]
|
40
|
+
def go(request)
|
41
|
+
# rejected at pattern
|
42
|
+
return reject(request) unless @pattern.match?(request.path)
|
43
|
+
|
44
|
+
request.push_state
|
45
|
+
path, splat, param = @pattern.match(request.path)
|
46
|
+
do_filepath(request, request.path.delete_suffix(path))
|
47
|
+
request.path = path
|
48
|
+
request.splat.append(*splat)
|
49
|
+
request.param.merge!(param)
|
50
|
+
value = process(request)
|
51
|
+
# rejected at callback - restore state
|
52
|
+
request.pop_state unless value
|
53
|
+
# finally, return process value
|
54
|
+
value
|
55
|
+
end
|
56
|
+
|
57
|
+
# Method callback on failed request navigation
|
58
|
+
# @param _request [Landline::Request]
|
59
|
+
# @return false
|
60
|
+
def reject(_request)
|
61
|
+
false
|
62
|
+
end
|
63
|
+
|
64
|
+
# Method callback on successful request navigation
|
65
|
+
# @param _request [Landline::Request]
|
66
|
+
# @return true
|
67
|
+
def process(_request)
|
68
|
+
true
|
69
|
+
end
|
70
|
+
|
71
|
+
attr_reader :remap, :root
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
# Process filepath for request
|
76
|
+
def do_filepath(request, path)
|
77
|
+
return unless @modify_filepath
|
78
|
+
|
79
|
+
if @root
|
80
|
+
request.filepath = "#{@root}/#{@remap ? '' : path}/"
|
81
|
+
else
|
82
|
+
request.filepath += "/#{path}/"
|
83
|
+
end
|
84
|
+
request.filepath.gsub!(/\/+/, "/")
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|