syntropy 0.1 → 0.3

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: 9f54331b249d5f3f5b113380173af8e6179590a60ea5f9f3461b1c87f2c4ce53
4
- data.tar.gz: f04553e5a04480e9720319a4b93dacdc38e04990a3e60471cf83fde1eea905eb
3
+ metadata.gz: 43d7686b3b9f5f09c819bebb9e46fdceb45c8c0a66441b1e12b130fa376defc5
4
+ data.tar.gz: f616e5dcdedc8a08fb23d5a52c1a92296cd348f8555fede1ac31ba0859c1be28
5
5
  SHA512:
6
- metadata.gz: 3c749127a9b98be60618f2d82229bcab09894465caebeef37a04672f5456e1480089cbbb9cb662969d024ed64a1657d6e1cbad96af18a2292c2cc5723628e358
7
- data.tar.gz: a0ab0555fff735b401204ee54f294c6c009d18d527c938da0ec467f82d0e30ede3e6a8bc76615ef2d8ba5fc01fcd6cd060f3e5522aaf13e69cf6c31fcfaf704b
6
+ metadata.gz: 1f6d14df3cbbaa74d9ec897cb50fd35caed0f48b6410f2abb13fad79947b50bbf33c32aa6358ab6464ac5b90e861fffacb345805d487cd67f5bd77b36cd7295b
7
+ data.tar.gz: af07c19a78d94fe37403246b302f95f047aeb12c764e8914e43f5ed1e1b1245cc0678a01e964e895fdc7aef0e775194c6ee9744939113352db07eca166693cc1
data/.rubocop.yml ADDED
@@ -0,0 +1,196 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.2
3
+ RubyInterpreters:
4
+ - ruby
5
+ Exclude:
6
+ - "**/*.gemspec"
7
+ - "test/**/*.rb"
8
+ - "examples/**/*.rb"
9
+ - "Gemfile*"
10
+ # Style/LambdaCall:
11
+ # Enabled: false
12
+ # Style/ModuleFunction:
13
+ # Enabled: false
14
+ # Style/RegexpLiteral:
15
+ # Enabled: false
16
+
17
+ # Naming/MemoizedInstanceVariableName:
18
+ # Enabled: false
19
+
20
+ # Style/Alias:
21
+ # EnforcedStyle: prefer_alias_method
22
+
23
+ # Style/SpecialGlobalVars:
24
+ # Enabled: false
25
+
26
+ # Style/ClassAndModuleChildren:
27
+ # Enabled: false
28
+
29
+ # Metrics/AbcSize:
30
+ # Enabled: false
31
+
32
+ # Style/MixinUsage:
33
+ # Enabled: false
34
+
35
+ # Style/MultilineBlockChain:
36
+ # Enabled: false
37
+
38
+ Lint/RescueException:
39
+ Enabled: false
40
+
41
+ # Lint/InheritException:
42
+ # Enabled: false
43
+
44
+ Style/NumericPredicate:
45
+ Enabled: false
46
+
47
+ # Style/TrivialAccessors:
48
+ # Enabled: false
49
+
50
+ # Lint/MissingSuper:
51
+ # Enabled: false
52
+
53
+ # Style/GlobalVars:
54
+ # Exclude:
55
+ # - lib/polyphony/auto_run.rb
56
+ # - lib/polyphony/extensions/core.rb
57
+ # - examples/**/*.rb
58
+
59
+ Layout/HashAlignment:
60
+ Enabled: false
61
+ EnforcedColonStyle: table
62
+ EnforcedHashRocketStyle: table
63
+
64
+ # Naming/AccessorMethodName:
65
+ # Exclude:
66
+ # - lib/polyphony/extensions/fiber.rb
67
+ # - examples/**/*.rb
68
+
69
+ # Naming/MethodName:
70
+ # Exclude:
71
+ # - test/test_signal.rb
72
+
73
+ # Lint/SuppressedException:
74
+ # Exclude:
75
+ # - examples/**/*.rb
76
+
77
+ Metrics/MethodLength:
78
+ Max: 14
79
+ # Exclude:
80
+ # - lib/polyphony/extensions/io.rb
81
+ # - lib/polyphony/extensions/fiber.rb
82
+ # - lib/polyphony/extensions/thread.rb
83
+ # - lib/polyphony/adapters/open3.rb
84
+ # - test/**/*.rb
85
+ # - examples/**/*.rb
86
+
87
+ # Metrics/ModuleLength:
88
+ # Exclude:
89
+ # - lib/polyphony/core/global_api.rb
90
+ # - examples/**/*.rb
91
+
92
+ # Metrics/ClassLength:
93
+ # Exclude:
94
+ # - lib/polyphony/extensions/io.rb
95
+ # - lib/polyphony/extensions/fiber.rb
96
+ # - lib/polyphony/extensions/object.rb
97
+ # - lib/polyphony/extensions/thread.rb
98
+ # - test/**/*.rb
99
+ # - examples/**/*.rb
100
+
101
+ # Metrics/CyclomaticComplexity:
102
+ # Exclude:
103
+ # - lib/polyphony/extensions/fiber.rb
104
+
105
+ # Metrics/PerceivedComplexity:
106
+ # Exclude:
107
+ # - lib/polyphony/extensions/fiber.rb
108
+
109
+ # Style/RegexpLiteral:
110
+ # Enabled: false
111
+
112
+ Style/RescueModifier:
113
+ Enabled: false
114
+ # Style/Documentation:
115
+ # Exclude:
116
+ # - test/**/*.rb
117
+ # - examples/**/*.rb
118
+ # - lib/polyphony/adapters/**/*.rb
119
+
120
+ # Style/FormatString:
121
+ # Exclude:
122
+ # - test/**/*.rb
123
+ # - examples/**/*.rb
124
+
125
+ # Style/FormatStringToken:
126
+ # Exclude:
127
+ # - test/**/*.rb
128
+ # - examples/**/*.rb
129
+
130
+ Naming/MethodParameterName:
131
+ Enabled: false
132
+
133
+ # Security/MarshalLoad:
134
+ # Exclude:
135
+ # - examples/**/*.rb
136
+
137
+ # Lint/ShadowedArgument:
138
+ # Exclude:
139
+ # - lib/polyphony/extensions/fiber.rb
140
+
141
+ # Style/HashEachMethods:
142
+ # Enabled: true
143
+
144
+ # Style/HashTransformKeys:
145
+ # Enabled: true
146
+
147
+ # Style/HashTransformValues:
148
+ # Enabled: true
149
+
150
+ # Layout/EmptyLinesAroundAttributeAccessor:
151
+ # Enabled: true
152
+
153
+ # Layout/SpaceAroundMethodCallOperator:
154
+ # Enabled: true
155
+
156
+ # Lint/DeprecatedOpenSSLConstant:
157
+ # Enabled: true
158
+
159
+ # Lint/MixedRegexpCaptureTypes:
160
+ # Enabled: true
161
+
162
+ # Lint/RaiseException:
163
+ # Enabled: true
164
+
165
+ # Lint/StructNewOverride:
166
+ # Enabled: true
167
+
168
+ Style/NegatedIf:
169
+ Enabled: false
170
+ # Style/NegatedWhile:
171
+ # Enabled: false
172
+
173
+ # Style/CombinableLoops:
174
+ # Enabled: false
175
+
176
+ # Style/InfiniteLoop:
177
+ # Enabled: false
178
+
179
+ # Style/RedundantReturn:
180
+ # Enabled: false
181
+
182
+ # Style/ExponentialNotation:
183
+ # Enabled: true
184
+
185
+ # Style/RedundantRegexpCharacterClass:
186
+ # Enabled: true
187
+
188
+ # Style/RedundantRegexpEscape:
189
+ # Enabled: true
190
+
191
+ # Style/SlicingWithRange:
192
+ # Enabled: true
193
+
194
+ # Style/RaiseArgs:
195
+ # Exclude:
196
+ # - lib/polyphony/extensions/fiber.rb
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## 0.3 2025-06-25
2
+
3
+ - Implement module reloading on file change
4
+
5
+ ## 0.2 2025-06-24
6
+
7
+ - Add CLI tool
8
+ - Implement basic module loading
9
+ - Implement ConnectionPool
10
+
1
11
  ## 0.1 2025-06-17
2
12
 
3
13
  - Move context inside Request object
data/TODO.md CHANGED
@@ -1,11 +0,0 @@
1
- - More database methods:
2
-
3
- - `Database#quote`
4
- - `Database#cache_flush` https://sqlite.org/c3ref/db_cacheflush.html
5
- - `Database#release_memory` https://sqlite.org/c3ref/db_release_memory.html
6
-
7
- - Security
8
-
9
- - Enable extension loading by using
10
- [SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION](https://www.sqlite.org/c3ref/c_dbconfig_defensive.html#sqlitedbconfigenableloadextension)
11
- in order to prevent usage of `load_extension()` SQL function.
data/bin/syntropy ADDED
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'syntropy'
5
+ require 'optparse'
6
+
7
+ opts = {
8
+ banner: Syntropy::BANNER,
9
+ logger: true
10
+ }
11
+
12
+ parser = OptionParser.new do |o|
13
+ o.banner = 'Usage: syntropy [options] DIR'
14
+
15
+ o.on('-b', '--bind BIND', String,
16
+ 'Bind address (default: http://0.0.0.0:1234). You can specify this flag multiple times to bind to multiple addresses.') do
17
+ opts[:bind] ||= []
18
+ opts[:bind] << it
19
+ end
20
+
21
+ o.on('-s', '--silent', 'Silent mode') do
22
+ opts[:banner] = nil
23
+ opts[:logger] = nil
24
+ end
25
+
26
+ o.on('-w', '--watch', 'Watch for changed files') do
27
+ opts[:watch_files] = 0.1
28
+ end
29
+
30
+ o.on('-h', '--help', 'Show this help message') do
31
+ puts o
32
+ exit
33
+ end
34
+
35
+ o.on('-v', '--version', 'Show version') do
36
+ require 'syntropy/version'
37
+ puts "Syntropy version #{Syntropy::VERSION}"
38
+ exit
39
+ end
40
+ end
41
+
42
+ RubyVM::YJIT.enable rescue nil
43
+
44
+ begin
45
+ parser.parse!
46
+ rescue StandardError => e
47
+ puts e.message
48
+ puts e.backtrace.join("\n")
49
+ exit
50
+ end
51
+
52
+ opts[:location] = ARGV.shift || '.'
53
+
54
+ if !File.directory?(opts[:location])
55
+ puts "#{File.expand_path(opts[:location])} Not a directory"
56
+ exit
57
+ end
58
+
59
+
60
+
61
+ opts[:machine] = UM.new
62
+ opts[:logger] = opts[:logger] && TP2::Logger.new(opts[:machine], **opts)
63
+
64
+ app = Syntropy::App.new(opts[:machine], opts[:location], '/', opts)
65
+ TP2.run(opts) { app.call(it) }
data/lib/syntropy/app.rb CHANGED
@@ -1,20 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'qeweney'
4
- require 'syntropy/errors'
5
4
  require 'json'
6
5
  require 'papercraft'
7
6
 
7
+ require 'syntropy/errors'
8
+ require 'syntropy/file_watch'
9
+ require 'syntropy/module'
10
+
8
11
  module Syntropy
9
12
  class App
10
13
  attr_reader :route_cache
11
14
 
12
- def initialize(src_path, mount_path)
13
- @src_path = src_path
15
+ def initialize(machine, src_path, mount_path, opts = {})
16
+ @machine = machine
17
+ @src_path = File.expand_path(src_path)
14
18
  @mount_path = mount_path
15
19
  @route_cache = {}
20
+ @opts = opts
16
21
 
17
22
  @relative_path_re = calculate_relative_path_re(mount_path)
23
+ @machine.spin do
24
+ # we do startup stuff asynchronously, in order to first let TP2 do its
25
+ # setup tasks
26
+ @machine.sleep 0.25
27
+ @opts[:logger]&.call("Serving from #{File.expand_path(@src_path)}")
28
+ start_file_watcher if opts[:watch_files]
29
+ end
18
30
  end
19
31
 
20
32
  def find_route(path, cache: true)
@@ -39,6 +51,32 @@ module Syntropy
39
51
 
40
52
  private
41
53
 
54
+ def start_file_watcher
55
+ @opts[:logger]&.call('Watching for module file changes...', nil)
56
+ wf = @opts[:watch_files]
57
+ period = wf.is_a?(Numeric) ? wf : 0.1
58
+ @machine.spin do
59
+ Syntropy.file_watch(@machine, @src_path, period: period) do
60
+ @opts[:logger]&.call("Detected changed file: #{it}")
61
+ invalidate_cache(it)
62
+ rescue Exception => e
63
+ p e
64
+ p e.backtrace
65
+ exit!
66
+ end
67
+ end
68
+ end
69
+
70
+ def invalidate_cache(fn)
71
+ invalidated_keys = []
72
+ @route_cache.each do |k, v|
73
+ @opts[:logger]&.call("Invalidate cache for #{k}", nil)
74
+ invalidated_keys << k if v[:fn] == fn
75
+ end
76
+
77
+ invalidated_keys.each { @route_cache.delete(it) }
78
+ end
79
+
42
80
  def calculate_relative_path_re(mount_path)
43
81
  mount_path = '' if mount_path == '/'
44
82
  /^#{mount_path}(?:\/(.*))?$/
@@ -67,12 +105,12 @@ module Syntropy
67
105
 
68
106
  entry = find_file_entry_with_extension(fs_path)
69
107
  return entry if entry[:kind] != :not_found
70
-
108
+
71
109
  find_up_tree_module(path)
72
110
  end
73
111
 
74
112
  def file_entry(fn)
75
- { fn: fn, kind: FILE_KINDS[File.extname(fn)] || :static }
113
+ { fn: File.expand_path(fn), kind: FILE_KINDS[File.extname(fn)] || :static }
76
114
  end
77
115
 
78
116
  def find_index_entry(dir)
@@ -115,19 +153,23 @@ module Syntropy
115
153
  when :not_found
116
154
  req.respond('Not found', ':status' => Qeweney::Status::NOT_FOUND)
117
155
  when :static
118
- entry[:mime_type] ||= Qeweney::MimeTypes[File.extname(entry[:fn])]
119
- req.respond(IO.read(entry[:fn]), 'Content-Type' => entry[:mime_type])
156
+ respond_static(req, entry)
120
157
  when :markdown
121
158
  body = render_markdown(IO.read(entry[:fn]))
122
159
  req.respond(body, 'Content-Type' => 'text/html')
123
160
  when :module
124
- call_module(entry, req)
161
+ call_module(req, entry)
125
162
  else
126
- raise "Invalid entry kind"
163
+ raise 'Invalid entry kind'
127
164
  end
128
165
  end
129
166
 
130
- def call_module(entry, req)
167
+ def respond_static(req, entry)
168
+ entry[:mime_type] ||= Qeweney::MimeTypes[File.extname(entry[:fn])]
169
+ req.respond(IO.read(entry[:fn]), 'Content-Type' => entry[:mime_type])
170
+ end
171
+
172
+ def call_module(req, entry)
131
173
  entry[:code] ||= load_module(entry)
132
174
  if entry[:code] == :invalid
133
175
  req.respond(nil, ':status' => Qeweney::Status::INTERNAL_SERVER_ERROR)
@@ -138,19 +180,17 @@ module Syntropy
138
180
  rescue StandardError => e
139
181
  p e
140
182
  p e.backtrace
141
- req.respond(nil, ':status' => Qeweney::S*tatus::INTERNAL_SERVER_ERROR)
183
+ req.respond(nil, ':status' => Qeweney::Status::INTERNAL_SERVER_ERROR)
142
184
  end
143
185
 
144
186
  def load_module(entry)
145
- body = IO.read(entry[:fn])
146
- klass = Class.new
147
- o = klass.instance_eval(body, entry[:fn], 1)
187
+ loader = Syntropy::ModuleLoader.new(@src_path, @opts)
188
+ ref = entry[:fn].gsub(%r{^#{@src_path}\/}, '').gsub(/\.rb$/, '')
189
+ o = loader.load(ref)
190
+ # klass = Class.new
191
+ # o = klass.instance_eval(body, entry[:fn], 1)
148
192
 
149
- if o.is_a?(Papercraft::HTML)
150
- return wrap_template(o)
151
- else
152
- return o
153
- end
193
+ o.is_a?(Papercraft::HTML) ? wrap_template(o) : o
154
194
  end
155
195
 
156
196
  def wrap_template(templ)
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'extralite'
4
+
5
+ module Syntropy
6
+ class ConnectionPool
7
+ attr_reader :count
8
+
9
+ def initialize(machine, fn, max_conn)
10
+ @machine = machine
11
+ @fn = fn
12
+ @count = 0
13
+ @max_conn = max_conn
14
+ @queue = UM::Queue.new
15
+ @key = :"db_#{fn}"
16
+ end
17
+
18
+ def with_db
19
+ if (db = Thread.current[@key])
20
+ @machine.snooze
21
+ return yield(db)
22
+ end
23
+
24
+ db = checkout
25
+ begin
26
+ Thread.current[@key] = db
27
+ yield(db)
28
+ ensure
29
+ Thread.current[@key] = nil
30
+ checkin(db)
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def checkout
37
+ if @queue.count == 0 && @count < @max_conn
38
+ return create_db
39
+ end
40
+
41
+ @machine.shift(@queue)
42
+ end
43
+
44
+ def checkin(db)
45
+ @machine.push(@queue, db)
46
+ end
47
+
48
+ def create_db
49
+ db = Extralite::Database.new(@fn, wal: true)
50
+ setup_db(db)
51
+ @count += 1
52
+ db
53
+ end
54
+
55
+ def setup_db(db)
56
+ # setup WAL, sync
57
+ # setup concurrency stuff
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Syntropy
4
+ def self.file_watch(machine, *roots, period: 0.1, &block)
5
+ raise 'Missing root paths' if roots.empty?
6
+
7
+ require 'listen'
8
+
9
+ queue = Thread::Queue.new
10
+ listener = Listen.to(*roots) do |modified, added, removed|
11
+ fns = (modified + added + removed).uniq
12
+ fns.each { queue.push(it) }
13
+ end
14
+ listener.start
15
+
16
+ loop do
17
+ machine.sleep(period) while queue.empty?
18
+ fn = queue.shift
19
+ block.call(fn)
20
+ end
21
+ rescue StandardError => e
22
+ p e
23
+ p e.backtrace
24
+ ensure
25
+ listener.stop
26
+ end
27
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Syntropy
4
+ class ModuleLoader
5
+ def initialize(root, env)
6
+ @root = root
7
+ @env = env
8
+ @loaded = {}
9
+ end
10
+
11
+ def load(ref)
12
+ @loaded[ref] ||= load_module(ref)
13
+ end
14
+
15
+ private
16
+
17
+ def load_module(ref)
18
+ fn = File.join(@root, "#{ref}.rb")
19
+ raise RuntimeError, "File not found #{fn}" if !File.file?(fn)
20
+
21
+ mod_body = IO.read(fn)
22
+ mod_ctx = Class.new(Syntropy::Module)
23
+ mod_ctx.loader = self
24
+ # mod_ctx = .new(self, @env)
25
+ mod_ctx.module_eval(mod_body, fn, 1)
26
+
27
+ export_value = mod_ctx.__export_value__
28
+
29
+ case export_value
30
+ when nil
31
+ raise RuntimeError, 'No export found'
32
+ when Symbol
33
+ # TODO: verify export_value denotes a valid method
34
+ mod_ctx.new(@env)
35
+ when Proc
36
+ export_value
37
+ else
38
+ export_value.new(@env)
39
+ end
40
+ end
41
+ end
42
+
43
+ class Module
44
+ def initialize(env)
45
+ @env = env
46
+ end
47
+
48
+ def self.loader=(loader)
49
+ @loader = loader
50
+ end
51
+
52
+ def self.import(ref)
53
+ @loader.load(ref)
54
+ end
55
+
56
+ def self.export(ref)
57
+ @__export_value__ = ref
58
+ end
59
+
60
+ def self.__export_value__
61
+ @__export_value__
62
+ end
63
+ end
64
+ end
@@ -6,6 +6,10 @@ require 'json'
6
6
 
7
7
  module Syntropy
8
8
  class RPCAPI
9
+ def initialize(env)
10
+ @env = env
11
+ end
12
+
9
13
  def call(req)
10
14
  response, status = invoke(req)
11
15
  req.respond(
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Syntropy
4
- VERSION = '0.1'
4
+ VERSION = '0.3'
5
5
  end
data/lib/syntropy.rb CHANGED
@@ -1,9 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'qeweney'
4
+ require 'uringmachine'
5
+ require 'tp2'
4
6
 
5
7
  require 'syntropy/errors'
6
- # require 'syntropy/context'
8
+ require 'syntropy/connection_pool'
9
+ require 'syntropy/module'
7
10
  require 'syntropy/rpc_api'
8
11
  require 'syntropy/app'
9
12
 
@@ -57,4 +60,23 @@ class Qeweney::Request
57
60
  end
58
61
 
59
62
  module Syntropy
63
+ def colorize(color_code)
64
+ "\e[#{color_code}m#{self}\e[0m"
65
+ end
66
+
67
+ GREEN = "\e[32m"
68
+ WHITE = "\e[0m"
69
+ YELLOW = "\e[33m"
70
+
71
+ BANNER = (
72
+ "\n"\
73
+ " #{GREEN}\n"\
74
+ " #{GREEN} ooo\n"\
75
+ " #{GREEN}ooooo\n"\
76
+ " #{GREEN} ooo vvv #{WHITE}Syntropy - a web framework for Ruby\n"\
77
+ " #{GREEN} o vvvvv #{WHITE}--------------------------------------\n"\
78
+ " #{GREEN} #{YELLOW}|#{GREEN} vvv o #{WHITE}https://github.com/noteflakes/syntropy\n"\
79
+ " #{GREEN} :#{YELLOW}|#{GREEN}:::#{YELLOW}|#{GREEN}::#{YELLOW}|#{GREEN}:\n"\
80
+ " #{YELLOW}++++++++++++\e[0m\n\n"
81
+ )
60
82
  end
data/syntropy.gemspec CHANGED
@@ -1,32 +1,37 @@
1
1
  require_relative './lib/syntropy/version'
2
2
 
3
3
  Gem::Specification.new do |s|
4
- s.version = Syntropy::VERSION
5
- s.licenses = ['MIT']
6
- s.author = 'Sharon Rosner'
7
- s.email = 'sharon@noteflakes.com'
8
- s.files = `git ls-files`.split
4
+ s.name = 'syntropy'
5
+ s.summary = 'Syntropic Web Framework'
6
+ s.version = Syntropy::VERSION
7
+ s.licenses = ['MIT']
8
+ s.author = 'Sharon Rosner'
9
+ s.email = 'sharon@noteflakes.com'
10
+ s.files = `git ls-files`.split
9
11
 
10
- s.homepage = 'https://github.com/noteflakes/syntropy'
11
- s.metadata = {
12
+ s.homepage = 'https://github.com/noteflakes/syntropy'
13
+ s.metadata = {
12
14
  'homepage_uri' => 'https://github.com/noteflakes/syntropy',
13
15
  'documentation_uri' => 'https://www.rubydoc.info/gems/syntropy',
14
16
  'changelog_uri' => 'https://github.com/noteflakes/syntropy/blob/master/CHANGELOG.md'
15
17
  }
16
- s.rdoc_options = ['--title', 'Extralite', '--main', 'README.md']
18
+ s.rdoc_options = ['--title', 'Extralite', '--main', 'README.md']
17
19
  s.extra_rdoc_files = ['README.md']
18
20
  s.require_paths = ['lib']
19
- s.required_ruby_version = '>= 3.2'
21
+ s.required_ruby_version = '>= 3.4'
22
+ s.executables = ['syntropy']
20
23
 
21
- s.add_dependency 'json', '2.12.2'
22
- s.add_dependency 'qeweney', '0.21'
23
- s.add_dependency 'papercraft', '1.4'
24
- s.add_dependency 'tp2', '0.11.3'
25
- s.add_dependency 'uringmachine', '0.14'
24
+ s.add_dependency 'extralite', '2.12'
25
+ s.add_dependency 'json', '2.12.2'
26
+ s.add_dependency 'papercraft', '1.4'
27
+ s.add_dependency 'qeweney', '0.21'
28
+ s.add_dependency 'tp2', '0.12.3.1'
29
+ s.add_dependency 'uringmachine', '0.15'
26
30
 
27
- s.add_development_dependency 'minitest', '5.25.5'
28
- s.add_development_dependency 'rake', '13.3.0'
31
+ s.add_dependency 'listen', '3.9.0'
32
+ s.add_dependency 'logger', '1.7.0'
33
+
34
+ s.add_development_dependency 'minitest', '5.25.5'
35
+ s.add_development_dependency 'rake', '13.3.0'
29
36
 
30
- s.name = 'syntropy'
31
- s.summary = 'Syntropic Web Framework'
32
37
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ Klass = import '_lib/klass'
4
+
5
+ def call(x)
6
+ Klass.foo.to_s * x
7
+ end
8
+
9
+ def bar
10
+ @env[:baz]
11
+ end
12
+
13
+ export :call
@@ -0,0 +1,15 @@
1
+ class Klass
2
+ def initialize(env)
3
+ @env = env
4
+ end
5
+
6
+ def foo
7
+ :bar
8
+ end
9
+
10
+ def bar
11
+ @env[:baz]
12
+ end
13
+ end
14
+
15
+ export Klass
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ Klass = import '_lib/klass'
4
+
5
+ def call
6
+ Klass.foo
7
+ end
@@ -1,3 +1,3 @@
1
- ->(req) {
1
+ export ->(req) {
2
2
  req.respond('About')
3
3
  }
data/test/app/api+.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  class API < Syntropy::RPCAPI
2
- def initialize
2
+ def initialize(env)
3
+ super(env)
3
4
  @count = 0
4
5
  end
5
6
 
@@ -9,11 +10,11 @@ class API < Syntropy::RPCAPI
9
10
 
10
11
  def incr!(req)
11
12
  if req.path != '/test/api'
12
- raise Syntropy::Error.new(Qeweney::Status::TEAPOT, 'Teapot')
13
+ raise Syntropy::Error.new(Qeweney::Status::TEAPOT, 'Teapot')
13
14
  end
14
15
 
15
16
  @count += 1
16
17
  end
17
18
  end
18
19
 
19
- API.new
20
+ export API
data/test/app/bar.rb CHANGED
@@ -1,3 +1,3 @@
1
- ->(req) {
1
+ export ->(req) {
2
2
  req.respond('foobar')
3
3
  }
data/test/app/tmp.rb ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ def call(req)
4
+ req.respond('foo')
5
+ end
6
+ export :call
data/test/test_app.rb CHANGED
@@ -6,7 +6,12 @@ class AppRoutingTest < Minitest::Test
6
6
  APP_ROOT = File.join(__dir__, 'app')
7
7
 
8
8
  def setup
9
- @app = Syntropy::App.new(APP_ROOT, '/test')
9
+ @machine = UM.new
10
+
11
+ @tmp_path = '/test/tmp'
12
+ @tmp_fn = File.join(APP_ROOT, "tmp.rb")
13
+
14
+ @app = Syntropy::App.new(@machine, APP_ROOT, '/test', watch_files: 0.05)
10
15
  end
11
16
 
12
17
  def full_path(fn)
@@ -100,7 +105,21 @@ class AppRoutingTest < Minitest::Test
100
105
 
101
106
  req = make_request(':method' => 'GET', ':path' => '/test/about/foo/bar')
102
107
  assert_equal Qeweney::Status::NOT_FOUND, req.response_status
108
+ end
109
+
110
+ def test_app_file_watching
111
+ @machine.sleep 0.2
112
+
113
+ req = make_request(':method' => 'GET', ':path' => @tmp_path)
114
+ assert_equal 'foo', req.response_body
103
115
 
116
+ orig_body = IO.read(@tmp_fn)
117
+ IO.write(@tmp_fn, orig_body.gsub('foo', 'bar'))
118
+ @machine.sleep(0.5)
104
119
 
120
+ req = make_request(':method' => 'GET', ':path' => @tmp_path)
121
+ assert_equal 'bar', req.response_body
122
+ ensure
123
+ IO.write(@tmp_fn, orig_body) if orig_body
105
124
  end
106
125
  end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+
5
+ class ConnectionPoolTest < Minitest::Test
6
+ def setup
7
+ @machine = UM.new
8
+ @fn = "/tmp/#{rand(100000)}.db"
9
+ @cp = Syntropy::ConnectionPool.new(@machine, @fn, 4)
10
+
11
+ FileUtils.rm(@fn) rescue nil
12
+ @standalone_db = Extralite::Database.new(@fn)
13
+ @standalone_db.execute("create table foo (x,y, z)")
14
+ @standalone_db.execute("insert into foo values (1, 2, 3)")
15
+ end
16
+
17
+ def test_with_db
18
+ assert_equal 0, @cp.count
19
+
20
+ @cp.with_db do |db|
21
+ assert_kind_of Extralite::Database, db
22
+
23
+ records = db.query("select * from foo")
24
+ assert_equal [{x: 1, y: 2, z: 3}], records
25
+ end
26
+
27
+ assert_equal 1, @cp.count
28
+ @cp.with_db { |db| assert_kind_of Extralite::Database, db }
29
+ assert_equal 1, @cp.count
30
+
31
+ dbs = []
32
+ ff = (1..2).map { |i|
33
+ @machine.spin {
34
+ @cp.with_db { |db|
35
+ dbs << db
36
+ @machine.sleep(0.05)
37
+ db.execute("insert into foo values (?, ?, ?)", i * 10 + 1, i * 10 + 2, i * 10 + 3)
38
+ }
39
+ }
40
+ }
41
+ @machine.join(*ff)
42
+
43
+ assert_equal 2, dbs.size
44
+ assert_equal 2, dbs.uniq.size
45
+ assert_equal 2, @cp.count
46
+
47
+ records = @standalone_db.query("select * from foo order by x")
48
+ assert_equal [
49
+ {x: 1, y: 2, z: 3},
50
+ {x: 11, y: 12, z: 13},
51
+ {x: 21, y: 22, z: 23},
52
+ ], records
53
+
54
+
55
+ dbs = []
56
+ ff = (1..10).map { |i|
57
+ @machine.spin {
58
+ @cp.with_db { |db|
59
+ dbs << db
60
+ @machine.sleep(0.05 + rand * 0.05)
61
+ db.execute("insert into foo values (?, ?, ?)", i * 10 + 1, i * 10 + 2, i * 10 + 3)
62
+ }
63
+ }
64
+ }
65
+ @machine.join(*ff)
66
+
67
+ assert_equal 10, dbs.size
68
+ assert_equal 4, dbs.uniq.size
69
+ assert_equal 4, @cp.count
70
+ end
71
+
72
+ def test_with_db_reentrant
73
+ dbs = @cp.with_db do |db1|
74
+ @cp.with_db do |db2|
75
+ [db1, db2]
76
+ end
77
+ end
78
+
79
+ assert_equal 1, dbs.uniq.size
80
+ end
81
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require_relative 'helper'
5
+
6
+ class FileWatchTest < Minitest::Test
7
+ def setup
8
+ @machine = UM.new
9
+ @root = "/tmp/syntropy/#{rand(1000000).to_s(16)}"
10
+ FileUtils.mkdir_p(@root)
11
+ end
12
+
13
+ def test_file_watch
14
+ queue = UM::Queue.new
15
+
16
+ f = @machine.spin do
17
+ Syntropy.file_watch(@machine, @root, period: 0.01) { @machine.push(queue, it) }
18
+ end
19
+ @machine.sleep(0.05)
20
+ assert_equal 0, queue.count
21
+
22
+ fn = File.join(@root, 'foo.bar')
23
+ IO.write(fn, 'abc')
24
+ assert_equal fn, @machine.shift(queue)
25
+
26
+ fn = File.join(@root, 'foo.bar')
27
+ IO.write(fn, 'def')
28
+ assert_equal fn, @machine.shift(queue)
29
+
30
+ FileUtils.rm(fn)
31
+ assert_equal fn, @machine.shift(queue)
32
+ ensure
33
+ @machine.schedule(f, UM::Terminate)
34
+ # @machine.join(f)
35
+ end
36
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+
5
+ class ModuleTest < Minitest::Test
6
+ def setup
7
+ @machine = UM.new
8
+ @root = File.join(__dir__, 'app')
9
+ @env = { baz: 42 }
10
+ @loader = Syntropy::ModuleLoader.new(@root, @env)
11
+ end
12
+
13
+ def test_module_loading
14
+ mod = @loader.load('_lib/klass')
15
+ assert_equal :bar, mod.foo
16
+ assert_equal 42, mod.bar
17
+
18
+ assert_raises(RuntimeError) { @loader.load('_lib/foo') }
19
+ assert_raises(RuntimeError) { @loader.load('_lib/missing-export') }
20
+
21
+ mod = @loader.load('_lib/callable')
22
+ assert_kind_of Syntropy::Module, mod
23
+ assert_equal 'barbarbar', mod.call(3)
24
+ assert_raises(NoMethodError) { mod.foo(2) }
25
+ assert_equal 42, mod.bar
26
+
27
+ mod = @loader.load('_lib/klass')
28
+ assert_equal :bar, mod.foo
29
+ @env[:baz] += 1
30
+ assert_equal 43, mod.bar
31
+ end
32
+ end
data/test/test_rpc_api.rb CHANGED
@@ -15,7 +15,7 @@ class RPCAPITest < Minitest::Test
15
15
  end
16
16
 
17
17
  def setup
18
- @app = TestAPI.new
18
+ @app = TestAPI.new({})
19
19
  end
20
20
 
21
21
  def test_rpc_api
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: syntropy
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.1'
4
+ version: '0.3'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharon Rosner
@@ -9,6 +9,20 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: extralite
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - '='
17
+ - !ruby/object:Gem::Version
18
+ version: '2.12'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - '='
24
+ - !ruby/object:Gem::Version
25
+ version: '2.12'
12
26
  - !ruby/object:Gem::Dependency
13
27
  name: json
14
28
  requirement: !ruby/object:Gem::Requirement
@@ -23,6 +37,20 @@ dependencies:
23
37
  - - '='
24
38
  - !ruby/object:Gem::Version
25
39
  version: 2.12.2
40
+ - !ruby/object:Gem::Dependency
41
+ name: papercraft
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - '='
45
+ - !ruby/object:Gem::Version
46
+ version: '1.4'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - '='
52
+ - !ruby/object:Gem::Version
53
+ version: '1.4'
26
54
  - !ruby/object:Gem::Dependency
27
55
  name: qeweney
28
56
  requirement: !ruby/object:Gem::Requirement
@@ -38,47 +66,61 @@ dependencies:
38
66
  - !ruby/object:Gem::Version
39
67
  version: '0.21'
40
68
  - !ruby/object:Gem::Dependency
41
- name: papercraft
69
+ name: tp2
42
70
  requirement: !ruby/object:Gem::Requirement
43
71
  requirements:
44
72
  - - '='
45
73
  - !ruby/object:Gem::Version
46
- version: '1.4'
74
+ version: 0.12.3.1
47
75
  type: :runtime
48
76
  prerelease: false
49
77
  version_requirements: !ruby/object:Gem::Requirement
50
78
  requirements:
51
79
  - - '='
52
80
  - !ruby/object:Gem::Version
53
- version: '1.4'
81
+ version: 0.12.3.1
54
82
  - !ruby/object:Gem::Dependency
55
- name: tp2
83
+ name: uringmachine
56
84
  requirement: !ruby/object:Gem::Requirement
57
85
  requirements:
58
86
  - - '='
59
87
  - !ruby/object:Gem::Version
60
- version: 0.11.3
88
+ version: '0.15'
61
89
  type: :runtime
62
90
  prerelease: false
63
91
  version_requirements: !ruby/object:Gem::Requirement
64
92
  requirements:
65
93
  - - '='
66
94
  - !ruby/object:Gem::Version
67
- version: 0.11.3
95
+ version: '0.15'
68
96
  - !ruby/object:Gem::Dependency
69
- name: uringmachine
97
+ name: listen
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - '='
101
+ - !ruby/object:Gem::Version
102
+ version: 3.9.0
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - '='
108
+ - !ruby/object:Gem::Version
109
+ version: 3.9.0
110
+ - !ruby/object:Gem::Dependency
111
+ name: logger
70
112
  requirement: !ruby/object:Gem::Requirement
71
113
  requirements:
72
114
  - - '='
73
115
  - !ruby/object:Gem::Version
74
- version: '0.14'
116
+ version: 1.7.0
75
117
  type: :runtime
76
118
  prerelease: false
77
119
  version_requirements: !ruby/object:Gem::Requirement
78
120
  requirements:
79
121
  - - '='
80
122
  - !ruby/object:Gem::Version
81
- version: '0.14'
123
+ version: 1.7.0
82
124
  - !ruby/object:Gem::Dependency
83
125
  name: minitest
84
126
  requirement: !ruby/object:Gem::Requirement
@@ -108,36 +150,48 @@ dependencies:
108
150
  - !ruby/object:Gem::Version
109
151
  version: 13.3.0
110
152
  email: sharon@noteflakes.com
111
- executables: []
153
+ executables:
154
+ - syntropy
112
155
  extensions: []
113
156
  extra_rdoc_files:
114
157
  - README.md
115
158
  files:
116
159
  - ".github/workflows/test.yml"
117
160
  - ".gitignore"
161
+ - ".rubocop.yml"
118
162
  - CHANGELOG.md
119
163
  - Gemfile
120
164
  - LICENSE
121
165
  - README.md
122
166
  - Rakefile
123
167
  - TODO.md
168
+ - bin/syntropy
124
169
  - lib/syntropy.rb
125
170
  - lib/syntropy/app.rb
126
- - lib/syntropy/context.rb
171
+ - lib/syntropy/connection_pool.rb
127
172
  - lib/syntropy/errors.rb
173
+ - lib/syntropy/file_watch.rb
174
+ - lib/syntropy/module.rb
128
175
  - lib/syntropy/rpc_api.rb
129
176
  - lib/syntropy/version.rb
130
177
  - syntropy.gemspec
131
178
  - test/app/_layout/default.rb
179
+ - test/app/_lib/callable.rb
180
+ - test/app/_lib/klass.rb
181
+ - test/app/_lib/missing-export.rb
132
182
  - test/app/about/foo.md
133
183
  - test/app/about/index.rb
134
184
  - test/app/api+.rb
135
185
  - test/app/assets/style.css
136
186
  - test/app/bar.rb
137
187
  - test/app/index.html
188
+ - test/app/tmp.rb
138
189
  - test/helper.rb
139
190
  - test/run.rb
140
191
  - test/test_app.rb
192
+ - test/test_connection_pool.rb
193
+ - test/test_file_watch.rb
194
+ - test/test_module.rb
141
195
  - test/test_rpc_api.rb
142
196
  - test/test_validation.rb
143
197
  homepage: https://github.com/noteflakes/syntropy
@@ -158,7 +212,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
158
212
  requirements:
159
213
  - - ">="
160
214
  - !ruby/object:Gem::Version
161
- version: '3.2'
215
+ version: '3.4'
162
216
  required_rubygems_version: !ruby/object:Gem::Requirement
163
217
  requirements:
164
218
  - - ">="
@@ -1,58 +0,0 @@
1
- # # frozen_string_literal: true
2
-
3
- # require 'syntropy/errors'
4
-
5
- # module Syntropy
6
- # class Context
7
- # attr_reader :request
8
-
9
- # def initialize(request)
10
- # @request = request
11
- # end
12
-
13
- # def params
14
- # @request.query
15
- # end
16
-
17
- # def validate_param(name, *clauses)
18
- # value = @request.query[name]
19
- # clauses.each do |c|
20
- # valid = param_is_valid?(value, c)
21
- # raise(Syntropy::ValidationError, 'Validation error') if !valid
22
- # value = param_convert(value, c)
23
- # end
24
- # value
25
- # end
26
-
27
- # BOOL_REGEXP = /^(t|f|true|false|on|off|1|0|yes|no)$/
28
- # BOOL_TRUE_REGEXP = /^(t|true|on|1|yes)$/
29
- # INTEGER_REGEXP = /^[\+\-]?[0-9]+$/
30
- # FLOAT_REGEXP = /^[\+\-]?[0-9]+(\.[0-9]+)?$/
31
-
32
- # def param_is_valid?(value, cond)
33
- # if cond == :bool
34
- # return (value && value =~ BOOL_REGEXP)
35
- # elsif cond == Integer
36
- # return (value && value =~ INTEGER_REGEXP)
37
- # elsif cond == Float
38
- # return (value && value =~ FLOAT_REGEXP)
39
- # elsif cond.is_a?(Array)
40
- # return cond.any? { |c| param_is_valid?(value, c) }
41
- # end
42
-
43
- # cond === value
44
- # end
45
-
46
- # def param_convert(value, klass)
47
- # if klass == :bool
48
- # value = value =~ BOOL_TRUE_REGEXP ? true : false
49
- # elsif klass == Integer
50
- # value = value.to_i
51
- # elsif klass == Float
52
- # value = value.to_f
53
- # else
54
- # value
55
- # end
56
- # end
57
- # end
58
- # end