landline 0.12.0 → 0.13.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d4924570720bd14c3d6149435782f59a64cd7c36727da5a204462653125de65d
4
- data.tar.gz: 8f06e6d9194aee22eb39752047c548fda980964548b0f5c78f20200e7f441c6e
3
+ metadata.gz: 290ba72a21e71891ae33e361aea7afcff00d98429f96b303e061158c3bd34520
4
+ data.tar.gz: 545debbdab28e39eaa6f94e588bfcf371259cbcd6e33399517d7c3d3bd4393da
5
5
  SHA512:
6
- metadata.gz: 353d30b81860f95e23d542e3b954e3fd1be308fa1cbd534017518a02238d5a79e886cd65e7237380be660286795cfe134c703fb5c2b19765420a065a5400f5d1
7
- data.tar.gz: d9be7521e662b6ff25f5cf7ac9c98bfe7f6d38a174cf3b9d95110b2c8ff42c500cb0e916a4961dadb48263566aa58812e2b3b5973b0e15c458b69856d0460d25
6
+ metadata.gz: 3e0b7da7816dcfba10f2e3db537344b6057ca7377da5a6a9e3b8aa4892883d417339cce001935705ebf60816eb9a2965501de5adae111235e79c551b0ede6f5a
7
+ data.tar.gz: 2177431dd21d534a1936541f287647bb23ced5968bada8b7a642b44f5d21c16f4cc4690654a11ee6194d62dc7027511a819c69ab123ce81d562ee166cf20157f
data/README.md CHANGED
@@ -139,10 +139,8 @@ end
139
139
  class Application < Landline::App
140
140
  use TimerMiddleware
141
141
 
142
- setup do
143
- get "/hello" do
144
- "Hello world!"
145
- end
142
+ get "/hello" do
143
+ "Hello world!"
146
144
  end
147
145
  end
148
146
 
@@ -196,7 +194,7 @@ For things to render correctly, please install the `redcarpet` gem.
196
194
 
197
195
  ```plain
198
196
  Landline - an HTTP request pattern matching system
199
- Copyright (C) 2022 yessiest (yessiest@text.512mb.org)
197
+ Copyright (C) 2023-2024 yessiest (yessiest@text.512mb.org)
200
198
 
201
199
  This program is free software: you can redistribute it and/or modify
202
200
  it under the terms of the GNU General Public License as published by
data/STRUCTURE.md ADDED
@@ -0,0 +1,129 @@
1
+ # Landline library and application structure
2
+
3
+ ## File layout
4
+
5
+ - `/landline/lib/*.rb` - core of the library
6
+ - `/landline/lib/dsl/` - module namespaces for use by executable contexts
7
+ - `/landline/lib/path/` - nodes that extend the Path class (traversable
8
+ node/setup execution context)
9
+ - `/landline/lib/probe/` - nodes that extend the Probe class (executable
10
+ nodes/per-request execution context)
11
+ - `/landline/lib/template/` - template engine adapters
12
+ - `/landline/lib/util/` - utility classes and modules
13
+ - `/landline/lib/pattern_matching/` - classes that implement path pattern
14
+ matching (i.e. globs, regex and static traversal paths)
15
+
16
+ ## Architecture overview
17
+
18
+ The following chapter defines information that pertains to the overall
19
+ structure and interaction of landline's core abstractions - Nodes, Paths
20
+ and Probes.
21
+
22
+ For context: Node is an element of the path tree, and is another name for a
23
+ Probe (which is a leaf of the tree) or a path (which is an internal vertex
24
+ of the tree)
25
+
26
+ ```plaintext
27
+ Path +-> Path +-> Probe
28
+ | |
29
+ | +-> Probe
30
+ +-> Probe
31
+ |
32
+ |-> Path -> Probe
33
+ ```
34
+
35
+ ### Execution contexts
36
+
37
+ In Sinatra, which this library takes most of its influence from, every
38
+ request creates its own instance of the Sinatra application. This seemed
39
+ like an excessive measure to solve a trivial problem of race conditions on a
40
+ shared execution context, so to solve that, Landline introduces per-request
41
+ execution contexts, which have a life time that spans the duration of
42
+ request processing.
43
+
44
+ In short, there are two distinct execution contexts:
45
+
46
+ - Per-request execution context, which is used by blocks that process the
47
+ request and which is bound to that given request
48
+ - Setup execution context, in which the Landline node tree is constructed
49
+
50
+ For every Path-like node, the supplied block is executed in the setup
51
+ context bound to that path.
52
+
53
+ For every Probe-like node, the supplied block is executed in the per-request
54
+ context of the request that is passed to the probe.
55
+
56
+ These execution contexts also affect the receivers of the instance variable
57
+ write and read operations. Additionally, the two types have different
58
+ sets of available methods, and every DSL method has a description attached
59
+ which describes in which context that method can be used (this is due to the
60
+ limitations of the YARD parser and due to the need of showing method
61
+ descriptions in the IDE)
62
+
63
+ ### Request traversal
64
+
65
+ Requests in Landline traverse through a series of paths that determine
66
+ which route the request would take. All nodes are organized in a tree
67
+ structure, where the root of the tree is a Server object that handles
68
+ things likes localized jumps, request/response conversion and response
69
+ finalization, and root-level error/`die()` handling.
70
+
71
+ Every path-like node can be supplied with callbacks that perform
72
+ the following functions:
73
+
74
+ - filters - callbacks that filter incoming request objects
75
+ - preprocessors - callbacks that don't usually affect the routing decision
76
+ but can modify requests before they come through
77
+ - postprocessors - callbacks that execute either when a request exits a
78
+ given path after not finding any suitable probe-like node or when a
79
+ response to a request is being finalized
80
+ - pipeline - single callback that is injected in the execution stack that
81
+ can be used to catch throws and exceptions
82
+
83
+ (see `/examples/logging.ru`, `/examples/norxondor_gorgonax/config.ru`)
84
+
85
+ The execution contexts for every callback (except pipeline) are shared with
86
+ those of the probe-like execution contexts, meaning that an instance
87
+ variable written to in a filter or a preprocessor can be accessed from the
88
+ probe node or a postprocessor callback.
89
+
90
+ Once all preprocessors and filters are finished successfully, the request
91
+ is passed to all subsequent graph elements (either other Paths or Probes)
92
+ to check whether a given Node accepts that element.
93
+
94
+ If the subtree of the path contains at least one probe that accepts the
95
+ request, that probe finishes processing the request and either exits
96
+ the tree immediately by throwing the `:finish` signal with a response
97
+ or by returning a valid (not nil) response from the `process` method,
98
+ in which case the response ascends from the probe back to the root.
99
+
100
+ If none of the subsequent elements of a path accepted the request, the
101
+ path executes `die(404)` if `bounce` is not enabled, and if `bounce` is
102
+ enabled, the request exits from the path and proceeds to sibling
103
+ (adjacent) paths. (see `/examples/dirbounce.ru`)
104
+
105
+ A probe can finish the request by either throwing a response, by executing a
106
+ jump to another path, by explicitly dying with a status code or by throwing
107
+ an exception (which effectively causes `die(500)`)
108
+ (see `/examples/errorpages.ru`, `/examples/jumps.ru`)
109
+
110
+ If a jump is executed, the path of the request is rewritten locally, and the
111
+ request is fed through the server again for processing. This is effectively
112
+ a localized redirect which retains accumulated request processing
113
+ information (i.e. instance variables)
114
+ (see `/examples/jumps.ru`)
115
+
116
+ The tree can be changed at runtime, but it's generally not advisable to
117
+ depend on that fact. If, eventually, some solution will be found to
118
+ optimizing trees into linear routing paths, and if that solution is proven
119
+ to improve performance, this statement might become false.
120
+
121
+ ```plaintext
122
+ +--------------------------------------+
123
+ v |
124
+ run (obj) -> App#call -> Server#call +-> Path#go -> ... -> Probe#go +
125
+ | | (if app acts as a wrapper for another
126
+ <-----------------------+ | Rack server and if response is 404
127
+ (returns back to Rack server | with x-cascade header set)
128
+ if no jumps occured) +-> passthrough#call
129
+ ```
data/lib/landline/app.rb CHANGED
@@ -3,18 +3,20 @@
3
3
  module Landline
4
4
  # Rack application interface
5
5
  class App
6
- # TODO: fix this mess somehow (probably impossible)
7
-
8
- # @!parse include Landline::DSL::PathMethods
9
- # @!parse include Landline::DSL::PathConstructors
10
- # @!parse include Landline::DSL::ProbeConstructors
11
- # @!parse include Landline::DSL::ProbeMethods
12
- # @!parse include Landline::DSL::CommonMethods
13
6
  class << self
7
+ # TODO: fix this mess somehow (probably impossible)
8
+ # @!parse include Landline::DSL::PathMethods
9
+ # @!parse include Landline::DSL::PathConstructors
10
+ # @!parse include Landline::DSL::ProbeConstructors
11
+ # @!parse include Landline::DSL::ProbeMethods
12
+ # @!parse include Landline::DSL::CommonMethods
13
+
14
14
  # Duplicate used middleware for the subclassed app
15
15
  def inherited(subclass)
16
16
  super(subclass)
17
+ @setup_chain ||= []
17
18
  subclass.middleware = @middleware.dup
19
+ subclass.setup_chain = @setup_chain.dup
18
20
  end
19
21
 
20
22
  # Include a middleware in application
@@ -24,17 +26,30 @@ module Landline
24
26
  @middleware.append(middleware)
25
27
  end
26
28
 
27
- # Setup block
28
- # @param block [#call]
29
- def setup(&block)
30
- @setup_block = block
29
+ # Check if Server can respond to given symbol
30
+ def respond_to_missing?(symbol, _include_private)
31
+ Landline::ServerContext.instance_methods.include?(symbol) || super
31
32
  end
32
33
 
33
- attr_accessor :middleware, :setup_block
34
+ # Store applied app manipulations
35
+ def method_missing(symbol, *args, **params, &callback)
36
+ if Landline::ServerContext.instance_methods.include? symbol
37
+ @setup_chain.append([symbol, args, params, callback])
38
+ else
39
+ super(symbol, *args, **params, &callback)
40
+ end
41
+ end
42
+
43
+ attr_accessor :middleware, :setup_chain
34
44
  end
35
45
 
36
46
  def initialize(*args, **opts)
37
- @app = ::Landline::Server.new(*args, **opts, &self.class.setup_block)
47
+ setup_chain = self.class.setup_chain
48
+ @app = ::Landline::Server.new(*args, **opts) do
49
+ setup_chain.each do |symbol, cargs, cparams, callback|
50
+ send(symbol, *cargs, **cparams, &callback)
51
+ end
52
+ end
38
53
  self.class.middleware&.reverse_each do |cls|
39
54
  @app = cls.new(@app)
40
55
  end
@@ -9,14 +9,17 @@ module Landline
9
9
  # @param errorcode [Integer]
10
10
  # @param backtrace [Array(String), nil]
11
11
  # @raise [UncaughtThrowError] throws :finish to return back to Server
12
- def die(errorcode, backtrace: nil)
13
- throw :finish, [errorcode].append(
14
- *(@origin.properties["handle.#{errorcode}"] or
15
- @origin.properties["handle.default"]).call(
16
- errorcode,
17
- backtrace: backtrace
18
- )
12
+ def die(errorcode, backtrace: nil, error: nil)
13
+ response = Landline::Response.convert(
14
+ (@origin.properties["handle.#{errorcode}"] or
15
+ @origin.properties["handle.default"]).call(
16
+ errorcode,
17
+ backtrace: backtrace,
18
+ error: error
19
+ )
19
20
  )
21
+ response.status = errorcode if response.status == 200
22
+ throw :finish, response
20
23
  end
21
24
 
22
25
  # (in Landline::Probe context)
@@ -43,21 +43,21 @@ module Landline
43
43
  # @param path [String]
44
44
  def jump(path)
45
45
  @origin.request.path = path
46
- throw(:break, [307, { "x-internal-jump": true }, []])
46
+ throw(:finish, [307, { "x-internal-jump": true }, []])
47
47
  end
48
48
 
49
49
  # (in Landline::Probe context)
50
50
  # Do clientside request redirection via 302 code
51
51
  # @param path [String]
52
52
  def redirect(path)
53
- throw(:break, [302, { "location": path }, []])
53
+ throw(:finish, [302, { "location": path }, []])
54
54
  end
55
55
 
56
56
  # (in Landline::Probe context)
57
57
  # Do clientside request redirection via 307 code
58
58
  # @param path [String]
59
59
  def redirect_with_method(path)
60
- throw(:break, [307, { "location": path }, []])
60
+ throw(:finish, [307, { "location": path }, []])
61
61
  end
62
62
 
63
63
  alias code status
@@ -84,7 +84,7 @@ module Landline
84
84
  @session = Landline::Session::Session.new(
85
85
  request.cookies.dig('session', 0)&.value,
86
86
  proc do |value|
87
- delete_cookie("session", value)
87
+ delete_cookie("session")
88
88
  cookie("session", value)
89
89
  end
90
90
  )
@@ -24,28 +24,6 @@ module Landline
24
24
  @__listeners[event]&.delete(listener)
25
25
  end
26
26
 
27
- # Await for an event
28
- # @param event [Symbol, Array<Symbol>] event or array of events to wait for
29
- # @return [Array]
30
- # @sg-ignore
31
- def await(event)
32
- blocking = true
33
- output = nil
34
- listener = proc do |*data|
35
- output = data
36
- blocking = false
37
- end
38
- if event.is_a? Array
39
- event.each { |x| on(x, &listener) }
40
- else
41
- on(event, &listener)
42
- end
43
- while blocking; end
44
- return output[0] if output.is_a? Array and output.length == 1
45
-
46
- output
47
- end
48
-
49
27
  private
50
28
 
51
29
  # Trigger the queue clearing process
@@ -95,19 +73,6 @@ module Landline
95
73
  )
96
74
  @readable = true
97
75
  @writable = true
98
- @data = Queue.new
99
- on :message do |msg|
100
- @data.enq(msg)
101
- end
102
- end
103
-
104
- # Start the main loop for the eventifier
105
- # @return [void]
106
- def ready
107
- return if @ready
108
-
109
- _loop
110
- @ready = true
111
76
  end
112
77
 
113
78
  # Send data through websocket
@@ -125,31 +90,33 @@ module Landline
125
90
  type: type
126
91
  )
127
92
  @io.write(frame.to_s)
93
+ rescue Errno::EPIPE => e
94
+ @writable = false
95
+ _emit :error, e
96
+ close if @readable
97
+ nil
128
98
  end
129
99
 
130
100
  # Read data from socket synchronously
131
- # @return [String, nil] nil returned if socket closes
101
+ # @return [WebSocket::Frame::Base, nil] nil if socket received a close event
132
102
  def read
133
103
  unless @readable
134
104
  raise self.class::WebSocketError,
135
105
  "socket closed for reading"
136
106
  end
137
107
 
138
- @data.deq
108
+ _process_events(proc { _read })
139
109
  end
140
110
 
141
- # Close the socket for reading
142
- # @return [void]
143
- def close_read
144
- _emit :close
145
- @readable = false
146
- @io.close_read
147
- end
111
+ # Read data from socket without blocking
112
+ # @return [WebSocket::Frame::Base, nil] nil if socket received a close event
113
+ def read_nonblock
114
+ unless @readable
115
+ raise self.class::WebSocketError,
116
+ "socket closed for reading"
117
+ end
148
118
 
149
- # Close the socket for writing
150
- def close_write
151
- @writable = false
152
- @io.close_write
119
+ _process_events(proc { _read_nonblock })
153
120
  end
154
121
 
155
122
  # Establish a connection through handshake
@@ -179,58 +146,88 @@ module Landline
179
146
  handshake
180
147
  end
181
148
 
182
- # Close the socket
183
- # @return [void]
184
- def close
185
- _close
186
- @writable = false
149
+ # Close socket for reading
150
+ def close_read
151
+ raise WebSocketError, 'socket closed for reading' unless @readable
152
+
153
+ _emit :close
187
154
  @readable = false
155
+ @io.close_read
156
+ end
157
+
158
+ # Close socket for reading
159
+ def close_write
160
+ raise WebSocketError, 'socket closed for writing' unless @writable
161
+
162
+ write(nil, type: :close)
163
+ @writable = false
164
+ @io.close_write
165
+ end
166
+
167
+ # Close the socket entirely
168
+ def close
169
+ raise WebSocketError, 'socket closed' unless @writable or @readable
170
+
171
+ close_read if @readable
172
+ close_write if @writable
188
173
  end
189
174
 
175
+ # Obtain internal IO object
176
+ # @return [IO]
177
+ def to_io
178
+ io
179
+ end
180
+
181
+ attr_reader :io, :readable, :writable
182
+
190
183
  private
191
184
 
192
- # Event reading loop
193
- # @return [void]
194
- def _loop
195
- @thread = Thread.new do
196
- loop do
197
- msg = _read
198
- if msg and [:text, :binary].include? msg.type
199
- _emit :message, msg
200
- elsif msg and msg.type == :close
201
- _emit :__close, msg
202
- break
203
- end
185
+ # Process incoming websocket events
186
+ # @param next_frame [#call] callback to get the next frame
187
+ # @return [WebSocket::Frame::Base, nil]
188
+ def _process_events(next_frame)
189
+ loop do
190
+ frame = next_frame.call
191
+ return nil unless frame
192
+
193
+ case frame.type
194
+ when :binary, :text, :pong then return frame
195
+ when :ping
196
+ write frame.to_s, type: :pong
197
+ when :close
198
+ close_read
199
+ return nil
200
+ else raise WebSocketError, "unknown frame type #{frame.type}"
204
201
  end
205
- rescue IOError => e
206
- @writable = false
207
- _emit :error, e
208
- close
209
- ensure
210
- close_read
211
202
  end
212
203
  end
213
204
 
214
205
  # Receive data through websocket
215
206
  # @return [String] output from frame
216
207
  def _read
217
- while (char = @io.getc)
208
+ while (char = @io.read(1))
218
209
  @frame_parser << char
219
210
  frame = @frame_parser.next
220
211
  return frame if frame
221
212
  end
213
+ rescue Errno::ECONNRESET => e
214
+ _emit :error, e
215
+ close if @readable or @writable
216
+ nil
222
217
  end
223
218
 
224
- # Close the websocket
225
- # @return [void]
226
- def _close
227
- frame = ::WebSocket::Frame::Outgoing::Server.new(
228
- version: @version,
229
- type: :close
230
- )
231
- @io.write(frame.to_s) if @writable
232
- sleep 0.1
233
- @io.close
219
+ # Receive data through websocket asynchronously
220
+ # @return [String] output from frame
221
+ def _read_nonblock
222
+ while (char = @io.read_nonblock(1))
223
+ @frame_parser << char
224
+ frame = @frame_parser.next
225
+ return frame if frame
226
+ end
227
+ rescue Errno::ECONNRESET => e
228
+ _emit :error, e
229
+ close if @readable or @writable
230
+ nil
234
231
  end
235
232
  end
236
233
  end
@@ -267,6 +264,7 @@ module Landline
267
264
  **@params
268
265
  )
269
266
  )
267
+ ""
270
268
  end
271
269
  end
272
270
  end
data/lib/landline/path.rb CHANGED
@@ -4,23 +4,9 @@ require_relative 'pattern_matching'
4
4
  require_relative 'node'
5
5
  require_relative 'dsl/constructors_path'
6
6
  require_relative 'dsl/methods_path'
7
- require_relative 'dsl/methods_common'
8
- require_relative 'dsl/methods_probe'
9
- require_relative 'dsl/constructors_probe'
10
7
  require_relative 'util/lookup'
11
8
 
12
9
  module Landline
13
- # Execution context for filters and preprocessors.
14
- class ProcessorContext
15
- include Landline::DSL::CommonMethods
16
- include Landline::DSL::ProbeMethods
17
- include Landline::DSL::ProbeConstructors
18
-
19
- def initialize(path)
20
- @origin = path
21
- end
22
- end
23
-
24
10
  # Execution context for path setup block.
25
11
  class PathContext
26
12
  include Landline::DSL::PathConstructors
@@ -31,21 +17,8 @@ module Landline
31
17
  end
32
18
  end
33
19
 
34
- # Ephemeral proxy class to which callback execution binds
35
- class PathExecutionOrigin
36
- def initialize(request, properties)
37
- @request = request
38
- @properties = Landline::Util::LookupROProxy.new(properties)
39
- end
40
-
41
- attr_accessor :response
42
- attr_reader :request, :properties
43
- end
44
-
45
20
  # Primary building block of request navigation.
46
21
  class Path < Landline::Node
47
- ExecutionOrigin = Landline::PathExecutionOrigin
48
- ProcContext = Landline::ProcessorContext
49
22
  Context = Landline::PathContext
50
23
 
51
24
  # @param path [Object] Object to generate {Landline::Pattern} from
@@ -64,6 +37,24 @@ module Landline
64
37
  context.instance_exec(&setup)
65
38
  end
66
39
 
40
+ # (see ::Landline::Node#go)
41
+ def go(request)
42
+ # This is done to allow pipeline to interject handlers
43
+ # I'm more than willing to admit that this is stupid,
44
+ # but it is well worth the logical flexibility.
45
+ if ['handle.default', 'handle.505'].any? do |x|
46
+ @properties.storage.include? x
47
+ end
48
+ begin
49
+ super(request)
50
+ rescue StandardError => e
51
+ _die(request, 500, backtrace: [e.to_s] + e.backtrace, error: e)
52
+ end
53
+ else
54
+ super(request)
55
+ end
56
+ end
57
+
67
58
  # Method callback on successful request navigation.
68
59
  # Finds the next appropriate path to go to.
69
60
  # @return [Boolean] true if further navigation will be done
@@ -108,8 +99,8 @@ module Landline
108
99
 
109
100
  # Create an execution context for in-path processing blocks
110
101
  def get_context(request)
111
- exec_origin = self.class::ExecutionOrigin.new(request, @properties)
112
- self.class::ProcContext.new(exec_origin)
102
+ request.context.origin.properties.lookup = @properties
103
+ request.context
113
104
  end
114
105
 
115
106
  # Sequentially run through all filters and drop request if one is false
@@ -118,7 +109,10 @@ module Landline
118
109
  def run_filters(request)
119
110
  proccontext = get_context(request)
120
111
  @filters.each do |filter|
121
- return false unless proccontext.instance_exec(request, &filter)
112
+ output = catch(:break) do
113
+ proccontext.instance_exec(request, &filter)
114
+ end
115
+ return false unless output
122
116
  end
123
117
  true
124
118
  end
@@ -128,8 +122,13 @@ module Landline
128
122
  def run_preprocessors(request)
129
123
  proccontext = get_context(request)
130
124
  @preprocessors.each do |preproc|
131
- proccontext.instance_exec(request, &preproc)
125
+ output = catch(:break) do
126
+ proccontext.instance_exec(request, &preproc)
127
+ true
128
+ end
129
+ return false unless output
132
130
  end
131
+ true
133
132
  end
134
133
 
135
134
  # Append postprocessors to request
@@ -146,7 +145,8 @@ module Landline
146
145
  def process_wrapped(request)
147
146
  return false unless run_filters(request)
148
147
 
149
- run_preprocessors(request)
148
+ return false unless run_preprocessors(request)
149
+
150
150
  enqueue_postprocessors(request)
151
151
  @children.each do |x|
152
152
  value = x.go(request)
@@ -155,9 +155,7 @@ module Landline
155
155
  value = index(request)
156
156
  return exit_stack(request, value) if value
157
157
 
158
- @bounce ? exit_stack(request) : _die(404)
159
- rescue StandardError => e
160
- _die(request, 500, backtrace: [e.to_s] + e.backtrace)
158
+ notfound(request)
161
159
  end
162
160
 
163
161
  # Run enqueued postprocessors on navigation failure
@@ -167,6 +165,11 @@ module Landline
167
165
  response
168
166
  end
169
167
 
168
+ # Exit with failure or throw 404
169
+ def notfound(request)
170
+ @bounce ? exit_stack(request) : _die(request, 404)
171
+ end
172
+
170
173
  # Try to perform indexing on the path if possible
171
174
  # @param request [Landline::Request]
172
175
  # @return [Boolean] true if indexing succeeded
@@ -188,16 +191,19 @@ module Landline
188
191
  # @param errorcode [Integer]
189
192
  # @param backtrace [Array(String), nil]
190
193
  # @raise [UncaughtThrowError] throws :finish to stop processing
191
- def _die(request, errorcode, backtrace: nil)
194
+ def _die(request, errorcode, backtrace: nil, error: nil)
192
195
  proccontext = get_context(request)
193
- throw :finish, [errorcode].append(
194
- *proccontext.instance_exec(
196
+ response = Landline::Response.convert(
197
+ proccontext.instance_exec(
195
198
  errorcode,
196
199
  backtrace: backtrace,
200
+ error: error,
197
201
  &(@properties["handle.#{errorcode}"] or
198
202
  @properties["handle.default"])
199
203
  )
200
204
  )
205
+ response.status = errorcode if response.status == 200
206
+ throw :finish, response
201
207
  end
202
208
  end
203
209
  end
@@ -35,7 +35,7 @@ module Landline
35
35
  def match(input)
36
36
  if @pattern.is_a? String
37
37
  input = Landline::PatternMatching.canonicalize(input)
38
- if input.start_with?(@pattern)
38
+ if _match?(input)
39
39
  [input.delete_prefix(@pattern), [], {}]
40
40
  else
41
41
  false
@@ -51,7 +51,8 @@ module Landline
51
51
  # @return [Boolean]
52
52
  def match?(input)
53
53
  if @pattern.is_a? String
54
- Landline::PatternMatching.canonicalize(input).start_with? @pattern
54
+ input = Landline::PatternMatching.canonicalize(input)
55
+ _match?(input)
55
56
  else
56
57
  @pattern.match?(input)
57
58
  end
@@ -59,6 +60,13 @@ module Landline
59
60
 
60
61
  private
61
62
 
63
+ def _match?(input)
64
+ parts = input.split("/")
65
+ @pattern.split("/").map.with_index do |part, index|
66
+ parts[index] == part
67
+ end.all?(true)
68
+ end
69
+
62
70
  def patternify(pattern)
63
71
  classdomain = Landline::PatternMatching
64
72
  classdomain.constants
@@ -29,8 +29,8 @@ module Landline
29
29
  # @return [Boolean] true if further navigation is possible
30
30
  # @raise [UncaughtThrowError] may raise if die() is called.
31
31
  def process(request)
32
- origin = Landline::ProbeExecutionOrigin.new(request, @properties)
33
- context = Landline::ProbeContext.new(origin)
32
+ origin, context = get_context(request)
33
+
34
34
  return reject(request) unless request.path.match?(/^\/?$/)
35
35
 
36
36
  response = catch(:break) do
@@ -47,6 +47,14 @@ module Landline
47
47
  end
48
48
  throw :finish, response
49
49
  end
50
+
51
+ private
52
+
53
+ # Create a context to run handler in
54
+ def get_context(request)
55
+ request.context.origin.properties.lookup = @properties
56
+ [request.context.origin, request.context]
57
+ end
50
58
  end
51
59
  end
52
60
  end
@@ -24,28 +24,6 @@ module Landline
24
24
  autoload :Link, "landline/probe/crosscall_handler"
25
25
  end
26
26
 
27
- # Context that provides execution context for Probes.
28
- class ProbeContext
29
- include Landline::DSL::ProbeConstructors
30
- include Landline::DSL::ProbeMethods
31
- include Landline::DSL::CommonMethods
32
-
33
- def initialize(origin)
34
- @origin = origin
35
- end
36
- end
37
-
38
- # Ephemeral proxy class to which callback execution binds
39
- class ProbeExecutionOrigin
40
- def initialize(request, properties)
41
- @request = request
42
- @properties = Landline::Util::LookupROProxy.new(properties)
43
- end
44
-
45
- attr_accessor :response
46
- attr_reader :request, :properties
47
- end
48
-
49
27
  # Test probe. Also base for all "reactive" nodes.
50
28
  class Probe < Landline::Node
51
29
  # @param path [Object]
@@ -3,6 +3,7 @@
3
3
  require 'uri'
4
4
  require_relative 'util/query'
5
5
  require_relative 'util/cookie'
6
+ require_relative 'sandbox'
6
7
 
7
8
  module Landline
8
9
  # Request wrapper for Rack protocol
@@ -30,13 +31,16 @@ module Landline
30
31
  @states = []
31
32
  # Postprocessors for current request
32
33
  @postprocessors = []
34
+ # Execution context
35
+ @context = init_context
33
36
  end
34
37
 
35
38
  # Run postprocessors
36
39
  # @param response [Landline::Response]
37
40
  def run_postprocessors(response)
41
+ @context.origin.properties.lookup = {}
38
42
  @postprocessors.reverse_each do |postproc|
39
- postproc.call(self, response)
43
+ @context.instance_exec(self, response, &postproc)
40
44
  end
41
45
  @postprocessors = []
42
46
  end
@@ -90,11 +94,17 @@ module Landline
90
94
 
91
95
  attr_reader :request_method, :script_name, :path_info, :server_name,
92
96
  :server_port, :server_protocol, :headers, :param, :splat,
93
- :postprocessors, :cookies, :rack
97
+ :postprocessors, :cookies, :rack, :context
94
98
  attr_accessor :path, :filepath, :query
95
99
 
96
100
  private
97
101
 
102
+ # Initialize execution context
103
+ def init_context
104
+ origin = Landline::ProcessorOrigin.new(self, {})
105
+ Landline::ProcessorContext.new(origin)
106
+ end
107
+
98
108
  # Initialize basic rack request parameters
99
109
  # @param env [Hash]
100
110
  def init_request_params(env)
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'dsl/methods_common'
4
+ require_relative 'dsl/methods_probe'
5
+ require_relative 'dsl/constructors_probe'
6
+ require_relative 'util/lookup'
7
+
8
+ module Landline
9
+ # Execution context for filters and preprocessors.
10
+ class ProcessorContext
11
+ include Landline::DSL::CommonMethods
12
+ include Landline::DSL::ProbeMethods
13
+ include Landline::DSL::ProbeConstructors
14
+
15
+ def initialize(path)
16
+ @origin = path
17
+ end
18
+
19
+ attr_reader :origin
20
+ end
21
+
22
+ # Ephemeral proxy class to which callback execution binds
23
+ class ProcessorOrigin
24
+ def initialize(request, properties)
25
+ @request = request
26
+ @properties = Landline::Util::LookupROProxy.new(properties)
27
+ end
28
+
29
+ attr_accessor :response
30
+ attr_reader :request, :properties
31
+ end
32
+ end
@@ -14,7 +14,7 @@ module Landline
14
14
  # @param parent [Landline::Node, nil] Parent object to inherit properties to
15
15
  # @param setup [#call] Setup block
16
16
  def initialize(passthrough = nil, parent: nil, **opts, &setup)
17
- super("", parent: nil, **opts, &setup)
17
+ super("", parent: parent, **opts, &setup)
18
18
  return if parent
19
19
 
20
20
  @passthrough = passthrough
@@ -58,14 +58,14 @@ module Landline
58
58
  def setup_properties(*_args, **_opts)
59
59
  {
60
60
  "index" => [],
61
- "handle.default" => proc do |code, backtrace: nil|
61
+ "handle.default" => proc do |code, backtrace: nil, **_extra|
62
62
  page = Landline::Util.default_error_page(code, backtrace)
63
63
  headers = {
64
64
  "content-length": page.bytesize,
65
65
  "content-type": "text/html",
66
66
  "x-cascade": true
67
67
  }
68
- [headers, page]
68
+ [code, headers, page]
69
69
  end,
70
70
  "path" => "/"
71
71
  }.each { |k, v| @properties[k] = v unless @properties[k] }
@@ -115,6 +115,8 @@ module Landline
115
115
 
116
116
  data.split(";").map do |cookiestr|
117
117
  key, value = cookiestr.match(/([^=]+)=?(.*)/).to_a[1..].map(&:strip)
118
+ next unless key and value
119
+
118
120
  cookie = Cookie.new(key, value)
119
121
  if hash[cookie.key]
120
122
  hash[cookie.key].append(cookie)
@@ -31,7 +31,7 @@ module Landline
31
31
  @storage[key] = value
32
32
  end
33
33
 
34
- attr_accessor :parent
34
+ attr_accessor :parent, :storage
35
35
  end
36
36
 
37
37
  # Read-only lookup proxy
@@ -46,6 +46,8 @@ module Landline
46
46
  def [](key)
47
47
  @lookup.[](key)
48
48
  end
49
+
50
+ attr_accessor :lookup
49
51
  end
50
52
  end
51
53
  end
@@ -28,7 +28,7 @@ module Landline
28
28
  # Decode charset parameter
29
29
  def decode(data)
30
30
  data = Landline::Util.unescape_html(data)
31
- return data unless self.headers['charset']
31
+ return data.force_encoding("UTF-8") unless self.headers['charset']
32
32
 
33
33
  data.force_encoding(self.headers['charset']).encode("UTF-8")
34
34
  end
@@ -15,12 +15,16 @@ module Landline
15
15
  PRINTCHAR = /[\x2-\x7E]/
16
16
  # Matches 1 or more CHARs excluding CTLs
17
17
  PRINTABLE = /#{PRINTCHAR}+/o
18
+ # !! RFC 6265 IS PROPOSED AND NOT AN IMPLEMENTED STANDARD YET !!
18
19
  # Matches the RFC6265 definition of a cookie-octet
19
- COOKIE_OCTET = /[\x21-\x7E&&[^",;\\]]*/
20
- COOKIE_VALUE = /(?:#{QUOTED}|#{COOKIE_OCTET})/o
21
- COOKIE_NAME = TOKEN
20
+ # COOKIE_VALUE = /(?:#{QUOTED}|#{COOKIE_OCTET})/o
21
+ # COOKIE_NAME = TOKEN
22
22
  # Matches the RFC6265 definition of cookie-pair.
23
23
  # Captures name (1) and value (2).
24
+ # !! RFC 6265 IS PROPOSED AND NOT AN IMPLEMENTED STANDARD YET !!
25
+ COOKIE_OCTET = /[\x21-\x7E&&[^",;\\]]*/
26
+ COOKIE_NAME = /[^;,=\s]*/
27
+ COOKIE_VALUE = /[^;,\s]*/
24
28
  COOKIE_PAIR = /\A(#{COOKIE_NAME})=(#{COOKIE_VALUE})\z/o
25
29
  # Matches a very abstract definition of a quoted header paramter.
26
30
  # Captures name (1) and value (2).
@@ -83,7 +87,7 @@ module Landline
83
87
  unless input.match? HeaderRegexp::PRINTABLE
84
88
  raise Landline::ParsingError, "input is not ascii printable"
85
89
  end
86
-
90
+
87
91
  opts.each do |key, value|
88
92
  check_param(key, value)
89
93
  newparam = if [String, Integer].include? value.class
data/lib/landline.rb CHANGED
@@ -12,11 +12,11 @@ require_relative 'landline/app'
12
12
  # Landline is a backend framework born as a by-product of experimentation
13
13
  module Landline
14
14
  # Landline version
15
- VERSION = '0.12 "Concrete and Gold" (pre-alpha)'
15
+ VERSION = '0.13.0 "Realign" (pre-alpha)'
16
16
 
17
17
  # Landline branding and version
18
18
  VLINE = "Landline/#{Landline::VERSION} (Ruby/#{RUBY_VERSION}/#{RUBY_RELEASE_DATE})\n".freeze
19
19
 
20
20
  # Landline copyright
21
- COPYRIGHT = "Copyright 2023 Yessiest"
21
+ COPYRIGHT = "Copyright 2023-2024 Yessiest"
22
22
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: landline
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.0
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yessiest
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-04-28 00:00:00.000000000 Z
11
+ date: 2024-12-10 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |
14
14
  Landline is a no-hard-dependencies HTTP routing DSL that was made entirely for fun.
@@ -21,10 +21,12 @@ extra_rdoc_files:
21
21
  - HACKING.md
22
22
  - LICENSE.md
23
23
  - README.md
24
+ - STRUCTURE.md
24
25
  files:
25
26
  - HACKING.md
26
27
  - LICENSE.md
27
28
  - README.md
29
+ - STRUCTURE.md
28
30
  - lib/landline.rb
29
31
  - lib/landline/app.rb
30
32
  - lib/landline/dsl/constructors_path.rb
@@ -48,6 +50,7 @@ files:
48
50
  - lib/landline/probe/serve_handler.rb
49
51
  - lib/landline/request.rb
50
52
  - lib/landline/response.rb
53
+ - lib/landline/sandbox.rb
51
54
  - lib/landline/server.rb
52
55
  - lib/landline/template.rb
53
56
  - lib/landline/template/erb.rb
@@ -81,7 +84,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
81
84
  - !ruby/object:Gem::Version
82
85
  version: '0'
83
86
  requirements: []
84
- rubygems_version: 3.5.3
87
+ rubygems_version: 3.5.16
85
88
  signing_key:
86
89
  specification_version: 4
87
90
  summary: Elegant HTTP DSL