syntropy 0.1 → 0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9f54331b249d5f3f5b113380173af8e6179590a60ea5f9f3461b1c87f2c4ce53
4
- data.tar.gz: f04553e5a04480e9720319a4b93dacdc38e04990a3e60471cf83fde1eea905eb
3
+ metadata.gz: a9a2e580917d4b05e8470695a75ab399326c6b8f7ee3b576526181c36791ee81
4
+ data.tar.gz: eb95e089e426bdb618981694cf8240911f30821a90d5ae9ef8f4bf4d79670a44
5
5
  SHA512:
6
- metadata.gz: 3c749127a9b98be60618f2d82229bcab09894465caebeef37a04672f5456e1480089cbbb9cb662969d024ed64a1657d6e1cbad96af18a2292c2cc5723628e358
7
- data.tar.gz: a0ab0555fff735b401204ee54f294c6c009d18d527c938da0ec467f82d0e30ede3e6a8bc76615ef2d8ba5fc01fcd6cd060f3e5522aaf13e69cf6c31fcfaf704b
6
+ metadata.gz: a85e2502bd313a10b5533ec5b8f124cfa72a3c4494f7ddcd912e0939973e2f72df458e6891543c5cbcf106718254561523c7f561ee1fd23bf348cab701696d4a
7
+ data.tar.gz: 60391470703441eccb1bdba52edfba754225171bcf65a08d146d01742e1ab8f7b8f6215cfc093ed4aee4b6e99c0f967cb9a6bf1f19274f6f4d34f8ae95a6e2c4
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,9 @@
1
+ ## 0.2 2025-06-24
2
+
3
+ - Add CLI tool
4
+ - Implement basic module loading
5
+ - Implement ConnectionPool
6
+
1
7
  ## 0.1 2025-06-17
2
8
 
3
9
  - Move context inside Request object
data/TODO.md CHANGED
@@ -1,11 +1,97 @@
1
- - More database methods:
1
+ ## Server tool
2
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
3
+ ```bash
4
+ $ bundle exec syntropy --dev ./site
5
+ $ bundle exec syntropy --workers 4 ./site
6
+ ```
6
7
 
7
- - Security
8
+ And also a config file:
8
9
 
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.
10
+ ```bash
11
+ $ bundle exec syntropy site.rb
12
+ ```
13
+
14
+ And the config file:
15
+
16
+ ```ruby
17
+ # site.rb
18
+ Syntropy.config do
19
+ root './site'
20
+ workers 4
21
+ log { |req| }
22
+ end
23
+ ```
24
+
25
+ ## Lightweight model API on top of Extralite
26
+
27
+ - DB connection pool
28
+ - Lightweight means 90% features with 10% effort:
29
+
30
+ ```ruby
31
+ Posts = Syntropy::Relation.new('posts')
32
+
33
+ posts = Posts.order_by(:stamp, :desc).all(db)
34
+
35
+ post = Posts.where(id: 13).first(db)
36
+
37
+ id = Posts.insert(db, title: 'foo', body: 'bar')
38
+
39
+ Posts.where(id: id).update(db, body: 'baz')
40
+
41
+ Posts.where(id: id).delete(db)
42
+ ```
43
+
44
+ The whole `db` argument thing is very limiting. For easier usage we integrate
45
+ the db connection pool as dependency injection the model:
46
+
47
+ ```ruby
48
+ db_pool = E2::ConnectionPool.new(fn)
49
+ Posts = Syntropy::Dataset.new(db_pool, 'posts')
50
+
51
+ Posts[id: 1] #=> { id: 1, title: 'foo', body: 'bar' }
52
+ Posts.find(id: 1) #=>
53
+
54
+ Posts.to_a #=> [...]
55
+ Posts.order_by(:stamp, :desc).to_a #=> [...]
56
+
57
+ id = Posts.insert(title: 'foo', body: 'bar')
58
+
59
+ post = Posts.where(id: id)
60
+ post.values #=> { id: 1, title: 'foo', body: 'bar' }
61
+ post.update(body: 'baz') #=> 1
62
+ post.delete
63
+ ```
64
+
65
+ So basically it's a bit similar to Sequel datasets, but there's no "object instance as single row". The instance is the entire set of rows in the table, or a subset thereof:
66
+
67
+ ```ruby
68
+ Posts.where(...).order_by(...).select(...).from(rowset)
69
+ ```
70
+
71
+ How about CTEs?
72
+
73
+ ```ruby
74
+ Users = Syntrop::Dataset.new(db_pool, 'users')
75
+
76
+ GroupIdRowset = Syntropy::Dataset {
77
+ with(
78
+ foo: Users,
79
+ bar: -> {
80
+ select user_id, group
81
+ from foo
82
+ },
83
+ baz: -> {
84
+ select id
85
+ from bar
86
+ where user_id == bar.select(:user_id)
87
+ }
88
+ )
89
+
90
+ select_all
91
+ from baz
92
+ where id == :group_id
93
+ }
94
+
95
+ users = GroupIdRowset.bind(group_id: 5).to_a
96
+
97
+ ```
data/bin/syntropy ADDED
@@ -0,0 +1,61 @@
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
+ opts[:machine] = UM.new
60
+ app = Syntropy::App.new(opts[:machine], opts[:location], '/', watch_files: 0.05)
61
+ TP2.run(opts) { app.(it) }
data/lib/syntropy/app.rb CHANGED
@@ -1,20 +1,34 @@
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)
15
+ def initialize(machine, src_path, mount_path, env = {})
16
+ @machine = machine
13
17
  @src_path = src_path
14
18
  @mount_path = mount_path
15
19
  @route_cache = {}
20
+ @env = env
16
21
 
17
22
  @relative_path_re = calculate_relative_path_re(mount_path)
23
+ if (wf = env[:watch_files])
24
+ period = wf.is_a?(Numeric) ? wf : 0.1
25
+ machine.spin do
26
+ Syntropy.file_watch(@machine, src_path, period: period) { invalidate_cache(it) }
27
+ rescue Exception => e
28
+ p e
29
+ p e.backtrace
30
+ end
31
+ end
18
32
  end
19
33
 
20
34
  def find_route(path, cache: true)
@@ -28,6 +42,15 @@ module Syntropy
28
42
  entry
29
43
  end
30
44
 
45
+ def invalidate_cache(fn)
46
+ invalidated_keys = []
47
+ @route_cache.each do |k, v|
48
+ invalidated_keys << k if v[:fn] == fn
49
+ end
50
+
51
+ invalidated_keys.each { @route_cache.delete(it) }
52
+ end
53
+
31
54
  def call(req)
32
55
  entry = find_route(req.path)
33
56
  render_entry(req, entry)
@@ -67,7 +90,7 @@ module Syntropy
67
90
 
68
91
  entry = find_file_entry_with_extension(fs_path)
69
92
  return entry if entry[:kind] != :not_found
70
-
93
+
71
94
  find_up_tree_module(path)
72
95
  end
73
96
 
@@ -138,13 +161,15 @@ module Syntropy
138
161
  rescue StandardError => e
139
162
  p e
140
163
  p e.backtrace
141
- req.respond(nil, ':status' => Qeweney::S*tatus::INTERNAL_SERVER_ERROR)
164
+ req.respond(nil, ':status' => Qeweney::Status::INTERNAL_SERVER_ERROR)
142
165
  end
143
166
 
144
167
  def load_module(entry)
145
- body = IO.read(entry[:fn])
146
- klass = Class.new
147
- o = klass.instance_eval(body, entry[:fn], 1)
168
+ loader = Syntropy::ModuleLoader.new(@src_path, @env)
169
+ ref = entry[:fn].gsub(%r{^#{@src_path}\/}, '').gsub(/\.rb$/, '')
170
+ o = loader.load(ref)
171
+ # klass = Class.new
172
+ # o = klass.instance_eval(body, entry[:fn], 1)
148
173
 
149
174
  if o.is_a?(Papercraft::HTML)
150
175
  return wrap_template(o)
@@ -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.2'
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.2'
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.2'
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.2
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.2
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