quince 0.5.1 โ†’ 0.6.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: b96b6d822903711930bf3baf921a25c7132761cbc339ff290ee1923aba16f4a6
4
- data.tar.gz: 9ea1dd9174b8e4f7aa100a83fb29aef865b4426d0889cc41e31b1d13c83cacd6
3
+ metadata.gz: f42655bc57603a51bd5231388dc7be387a51a8e912549f6a504fbaeb0e1dbb3e
4
+ data.tar.gz: ed87a57062ce7343b08409fd8126034a7dbf46e094b1741829fc63fdb9ab1f7d
5
5
  SHA512:
6
- metadata.gz: 59599fdd6fb5b40d606bcc2f70d636f0f214f10e14c424cd95d3b7c1dddef4341368d8569fff5ad5d5ebc77fcd0b865e274229d66b19665bb48ec54c62fca5f7
7
- data.tar.gz: b6ecdd02de862f7659ac94a3c588687fd2b265e7ceb2ed3cae8893587e5d4e37454a7d9015398baf1e67c82cde6141ec32383093a7799db466c438b3bd52e241
6
+ metadata.gz: 7c5358907ee716710a7c0f53c277ede1301ee16c31ab2304a63d33cdd91dff9f80a282f2832637d7b5ad14d4103d65a5f9ce16c4ed02cda14f9a148145263ee5
7
+ data.tar.gz: 0dc9bad86700382b48810bf70a2c0b0258c7e3383a4b73a37ea84fc46bf8ca174f2e20404c5070723512ffd2c1f0b1a61447bd9ce3da9eec6275e2dc8a003309
data/Gemfile.lock CHANGED
@@ -1,15 +1,26 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- quince (0.5.1)
4
+ quince (0.6.0)
5
5
  oj (~> 3.13)
6
+ rack-contrib (~> 2.3)
7
+ sinatra (~> 2.1)
8
+ sinatra-contrib (~> 2.1)
6
9
  typed_struct (>= 0.1.4)
7
10
 
8
11
  GEM
9
12
  remote: https://rubygems.org/
10
13
  specs:
11
14
  diff-lcs (1.4.4)
15
+ multi_json (1.15.0)
16
+ mustermann (1.1.1)
17
+ ruby2_keywords (~> 0.0.1)
12
18
  oj (3.13.9)
19
+ rack (2.2.3)
20
+ rack-contrib (2.3.0)
21
+ rack (~> 2.0)
22
+ rack-protection (2.1.0)
23
+ rack
13
24
  rake (13.0.6)
14
25
  rbs (1.6.2)
15
26
  rspec (3.10.0)
@@ -25,6 +36,19 @@ GEM
25
36
  diff-lcs (>= 1.2.0, < 2.0)
26
37
  rspec-support (~> 3.10.0)
27
38
  rspec-support (3.10.2)
39
+ ruby2_keywords (0.0.5)
40
+ sinatra (2.1.0)
41
+ mustermann (~> 1.0)
42
+ rack (~> 2.2)
43
+ rack-protection (= 2.1.0)
44
+ tilt (~> 2.0)
45
+ sinatra-contrib (2.1.0)
46
+ multi_json
47
+ mustermann (~> 1.0)
48
+ rack-protection (= 2.1.0)
49
+ sinatra (= 2.1.0)
50
+ tilt (~> 2.0)
51
+ tilt (2.0.10)
28
52
  typed_struct (0.1.4)
29
53
  rbs (~> 1.0)
30
54
 
data/README.md CHANGED
@@ -10,7 +10,8 @@ React, Turbo, Hotwire amongst others
10
10
 
11
11
  ### Current status
12
12
 
13
- Proof of concept, but [working in production](https://quince-rb.herokuapp.com/), and with decent performance despite few optimisations at this stage
13
+ Early, but [working in production](https://quince-rb.herokuapp.com/). Expect more features and
14
+ optimisations to come, but also potential for big changes between versions in the early stages.
14
15
 
15
16
  ### How it works
16
17
 
@@ -22,7 +23,7 @@ Proof of concept, but [working in production](https://quince-rb.herokuapp.com/),
22
23
 
23
24
  ```ruby
24
25
  # app.rb
25
- require "quince_sinatra"
26
+ require "quince"
26
27
 
27
28
  class App < Quince::Component
28
29
  def render
@@ -46,7 +47,7 @@ ruby app.rb
46
47
  ## More complex example
47
48
 
48
49
  ```ruby
49
- require 'quince_sinatra'
50
+ require 'quince'
50
51
 
51
52
  class App < Quince::Component
52
53
  def render
@@ -114,49 +115,23 @@ expose App, at: "/"
114
115
 
115
116
  ### Why?
116
117
 
117
- - Lightweight ๐Ÿชถ
118
- - Very few dependencies
119
- - Just a couple hundred lines of core logic
120
- - Fewer than 30 lines (unminified) of JavaScript in the front end
121
- - Plug and play into multiple back ends ๐Ÿ”Œ
122
118
  - Components > templates ๐Ÿงฉ
123
- - Write html-like elements, but with strong typo resistence
124
- - no special syntax or compilation required
119
+ - Lightweight ๐Ÿชถ
125
120
  - Shallow learning curve if you are already familiar with React ๐Ÿ“ˆ
126
- - Just worry about your core business logic and how the app looks ๐Ÿงช
127
- - No need to worry about
128
- - routes
129
- - controllers
130
- - front end -> back end communication/APIs/Data transfer
131
- - front end -> back end code sharing
132
- - Quince handles these for you
133
- - No node_modules ๐Ÿ“ฆ
134
- - No yarn/npm
135
- - Minimise bundle size concerns
136
- - Manage your dependencies just using bundler & rubygems
137
- - Make use of other pre-built Quince components via rubygems
121
+ - Focus on your core business logic, not routes/APIs/data transfer/code sharing ๐Ÿงช
122
+ - No compilation/node_modules/yarn/js bundle size concerns - just bundler ๐Ÿ“ฆ
138
123
  - Get full use of Ruby's rich and comprehensive standard library ๐Ÿ’Ž
139
124
  - Take advantage of Ruby's ability to wrap native libraries (eg. gems using C) โšก๏ธ
140
- - Fully server-rendered responses ๐Ÿ“ก
141
- - Single source of truth for your app's code (no code-sharing needed)
142
- - Better SEO out the box
143
- - Know exactly what your user is seeing
144
- - Tracking a user's activity on the front end has become a big deal, especially in heavily front-end driven apps/SPAs, in order to be able to see how a user is actually using the app (ie. to track how the state has been changing on the front end)
145
- - This normally requires cookies and a premium third party service
146
- - But if everything a user sees is generated server-side, it would be easy to reconstruct a user's journey and their state changes
125
+ - Fully server-rendered responses - single source of truth ๐Ÿ“ก
126
+ - Easy to recreate/rehydrate a pages state (almost nothing is stored in memory from JavaScript - all
127
+ the state is stored with the HTML document's markup)
147
128
 
148
129
  ## Installation
149
130
 
150
- Quince itself is framework agnostic, so you should use an adaptor which plugs it into an existing framework for handling basic server needs
151
-
152
- ### Install it via adapters
153
-
154
- - [Sinatra](https://github.com/johansenja/quince_sinatra)
155
-
156
- Pick one, and add it to your application's Gemfile, eg:
131
+ Add this to your application's Gemfile:
157
132
 
158
133
  ```ruby
159
- gem 'quince_sinatra'
134
+ gem 'quince'
160
135
  ```
161
136
 
162
137
  And then execute:
@@ -165,7 +140,7 @@ And then execute:
165
140
 
166
141
  Or install it yourself as:
167
142
 
168
- $ gem install quince_sinatra
143
+ $ gem install quince
169
144
 
170
145
 
171
146
  ## Usage notes
@@ -16,7 +16,7 @@ module Quince
16
16
 
17
17
  ATTRIBUTES_BY_ELEMENT = {
18
18
  "A" => {
19
- # download: t::Any,
19
+ download: opt_string_sym,
20
20
  href: opt_string_sym,
21
21
  hreflang: opt_string_sym,
22
22
  media: opt_string_sym,
@@ -337,7 +337,7 @@ module Quince
337
337
  "Area" => {
338
338
  alt: opt_string_sym,
339
339
  coords: opt_string_sym,
340
- # download: t::Any,
340
+ download: opt_string_sym,
341
341
  href: opt_string_sym,
342
342
  hreflang: opt_string_sym,
343
343
  media: opt_string_sym,
@@ -12,6 +12,7 @@ module Quince
12
12
  rerender: nil,
13
13
  push_params_state: nil,
14
14
  handle_errors: true,
15
+ download: false,
15
16
  }.freeze
16
17
 
17
18
  def callback(method_name, **opts)
@@ -29,7 +30,7 @@ module Quince
29
30
  attr_reader(
30
31
  :receiver,
31
32
  :method_name,
32
- *ComponentHelpers::DEFAULT_CALLBACK_OPTIONS.keys,
33
+ *ComponentHelpers::DEFAULT_CALLBACK_OPTIONS.keys,
33
34
  )
34
35
  end
35
36
 
@@ -1,3 +1,4 @@
1
+ require "forwardable"
1
2
  require_relative "callback"
2
3
 
3
4
  module Quince
@@ -24,18 +25,20 @@ module Quince
24
25
  @exposed_actions ||= Set.new
25
26
  @exposed_actions.add action
26
27
  route = "/api/#{self.name}/#{action}"
27
- Quince.middleware.create_route_handler(
28
+ Quince::SinatraApp.create_route_handler(
28
29
  verb: method,
29
30
  route: route,
30
- ) do |params|
31
+ ) do |bind|
32
+ Thread.current[:request_binding] = bind
33
+ params = bind.receiver.params
34
+ Thread.current[:params] = params[:params] || {}
31
35
  instance = Quince::Serialiser.deserialise(CGI.unescapeHTML(params[:component]))
32
- Quince::Component.class_variable_set :@@params, params[:params]
33
- render_with = if params[:rerender]
34
- instance.instance_variable_set :@state_container, params[:stateContainer]
35
- params[:rerender][:method].to_sym
36
- else
37
- :render
38
- end
36
+ if params[:rerender]
37
+ instance.instance_variable_set :@state_container, params[:stateContainer]
38
+ render_with = params[:rerender][:method].to_sym
39
+ else
40
+ render_with = :render
41
+ end
39
42
  instance.instance_variable_set :@render_with, render_with
40
43
  instance.instance_variable_set :@callback_event, params[:event]
41
44
  if @exposed_actions.member? action
@@ -73,11 +76,9 @@ module Quince
73
76
  end
74
77
  end
75
78
 
79
+ extend Forwardable
76
80
  include Callback::ComponentHelpers
77
81
 
78
- # set default
79
- @@params = {}
80
-
81
82
  attr_reader :props, :state, :children
82
83
 
83
84
  def render
@@ -91,9 +92,15 @@ module Quince
91
92
  end
92
93
 
93
94
  def params
94
- @@params
95
+ Thread.current[:params]
95
96
  end
96
97
 
98
+ def request_context
99
+ Thread.current[:request_binding].receiver
100
+ end
101
+
102
+ def_delegators :request_context, :attachment, :request, :response, :redirect, :halt
103
+
97
104
  private
98
105
 
99
106
  attr_reader :__id
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ ENV["RACK_ENV"] ||= "development"
4
+
5
+ require "sinatra/base"
6
+ require "sinatra/reloader" if ENV["RACK_ENV"] == "development"
7
+ require "rack/contrib"
8
+
9
+ module Quince
10
+ class SinatraApp < Sinatra::Base
11
+ configure :development do
12
+ if Object.const_defined? "Sinatra::Reloader"
13
+ register Sinatra::Reloader
14
+ dont_reload __FILE__
15
+ also_reload $0
16
+ end
17
+ end
18
+ use Rack::JSONBodyParser
19
+ use Rack::Deflater
20
+ set :public_folder, File.join(File.dirname(File.expand_path($0)), "public")
21
+
22
+ class << self
23
+ def create_route_handler(verb:, route:, &blck)
24
+ meth = case verb
25
+ when :POST, :post
26
+ :post
27
+ when :GET, :get
28
+ :get
29
+ else
30
+ raise "invalid verb"
31
+ end
32
+
33
+ public_send meth, route do
34
+ Quince.to_html(blck.call(binding))
35
+ ensure
36
+ Thread.current[:request_binding] = nil
37
+ Thread.current[:params] = nil
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ def expose(component, at:)
45
+ Quince::SinatraApp.get(at) do
46
+ Thread.current[:request_binding] = binding
47
+ Thread.current[:params] = binding.receiver.params
48
+ comp = component.instance_of?(Class) ? component.create : component
49
+ comp.instance_variable_set :@render_with, :render
50
+ Quince.to_html(comp)
51
+ ensure
52
+ Thread.current[:request_binding] = nil
53
+ Thread.current[:params] = nil
54
+ end
55
+ end
56
+
57
+ at_exit do
58
+ if $!.nil? || ($!.is_a?(SystemExit) && $!.success?)
59
+ if Object.const_defined? "Sinatra::Reloader"
60
+ app_dir = Pathname(File.expand_path($0)).dirname.to_s
61
+ $LOADED_FEATURES.each do |f|
62
+ next unless f.start_with? app_dir
63
+
64
+ Quince::SinatraApp.also_reload f
65
+ end
66
+ end
67
+
68
+ Quince::SinatraApp.run!
69
+ end
70
+ end
@@ -1,28 +1,5 @@
1
1
  module Quince
2
2
  class << self
3
- attr_reader :middleware
4
- attr_accessor :underlying_app
5
-
6
- def optional_string
7
- @optional_string ||= Rbs("String?")
8
- end
9
-
10
- def middleware=(middleware)
11
- @middleware = middleware
12
- Object.define_method(:expose) do |component, at:|
13
- Quince.middleware.create_route_handler(
14
- verb: :GET,
15
- route: at,
16
- ) do |params|
17
- component = component.create if component.instance_of? Class
18
- Quince::Component.class_variable_set :@@params, params
19
- component.instance_variable_set :@render_with, :render
20
- component
21
- end
22
- end
23
- Object.send :private, :expose
24
- end
25
-
26
3
  def define_constructor(const, constructor_name = nil)
27
4
  if const.name
28
5
  parts = const.name.split("::")
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Quince
4
- VERSION = "0.5.1"
4
+ VERSION = "0.6.0"
5
5
  end
data/lib/quince.rb CHANGED
@@ -5,3 +5,4 @@ require_relative "quince/singleton_methods"
5
5
  require_relative "quince/component"
6
6
  require_relative "quince/html_tag_components"
7
7
  require_relative "quince/version"
8
+ require_relative "quince/sinatra"
data/quince.gemspec CHANGED
@@ -29,6 +29,9 @@ Gem::Specification.new do |spec|
29
29
  spec.require_paths = ["lib"]
30
30
 
31
31
  # Uncomment to register a new dependency of your gem
32
+ spec.add_dependency "sinatra", "~> 2.1"
33
+ spec.add_dependency "sinatra-contrib", "~> 2.1"
34
+ spec.add_dependency "rack-contrib", "~> 2.3"
32
35
  spec.add_dependency "typed_struct", ">= 0.1.4"
33
36
  spec.add_dependency "oj", "~> 3.13"
34
37
 
data/scripts.js CHANGED
@@ -11,7 +11,12 @@ const Q = {
11
11
  }
12
12
  ).then(resp => {
13
13
  if (resp.status <= 299) {
14
- return resp.text()
14
+ const cd = resp.headers.get("Content-Disposition");
15
+ if (cd && cd.trim().startsWith("attachment")) {
16
+ return resp.blob();
17
+ } else {
18
+ return resp.text();
19
+ }
15
20
  } else if (resp.status >= 500) {
16
21
  throw Q.em["500"];
17
22
  } else {
@@ -21,45 +26,46 @@ const Q = {
21
26
 
22
27
  throw msg;
23
28
  }
24
- }).then(html => {
25
- const element = document.querySelector(selector);
26
- if (!element) {
27
- throw `element not found for ${selector}`;
28
- }
29
-
30
- switch (mode) {
31
- case "append_diff":
32
- const tmpElem = document.createElement(element.nodeName);
33
- tmpElem.innerHTML = html;
34
- const newNodes = Array.from(tmpElem.childNodes);
35
- const script = newNodes.pop();
36
- const existingChildren = element.childNodes;
37
- // This comparison doesn't currently work because of each node's unique id (data-quid).
38
- // maybe it would be possible to use regex replace to on the raw html, but it could also
39
- // be overkill
40
- // let c = 0;
41
- // for (; c < existingChildren.length; c++) {
42
- // if (existingChildren[c].isEqualNode(newNodes[c]))
43
- // continue;
44
- // else
45
- // break;
46
- // }
47
- // for the time being, we can just assume that we can just take the extra items
48
- let c = existingChildren.length;
49
- for (const node of newNodes.slice(c)) {
50
- element.appendChild(node);
29
+ }).then(data => {
30
+ switch (true) {
31
+ case data instanceof Blob:
32
+ const url = URL.createObjectURL(data);
33
+ const a = document.createElement('a');
34
+ a.href = url;
35
+ a.download = "download";
36
+ document.body.appendChild(a);
37
+ a.click();
38
+ a.remove();
39
+ break;
40
+ default: // html
41
+ const element = document.querySelector(selector);
42
+ if (!element) {
43
+ throw `element not found for ${selector}`;
51
44
  }
52
45
 
53
- const newScript = document.createElement("script");
54
- newScript.dataset.quid = script.dataset.quid;
55
- newScript.innerHTML = script.innerHTML;
56
- document.head.appendChild(newScript);
57
- break;
58
- case "replace":
59
- element.outerHTML = html;
60
- break;
61
- default:
62
- throw `mode ${mode} is not valid`;
46
+ switch (mode) {
47
+ case "append_diff":
48
+ const tmpElem = document.createElement(element.nodeName);
49
+ tmpElem.innerHTML = data;
50
+ const newNodes = Array.from(tmpElem.childNodes);
51
+ const script = newNodes.pop();
52
+ const existingChildren = element.childNodes;
53
+ let c = existingChildren.length;
54
+ for (const node of newNodes.slice(c)) {
55
+ element.appendChild(node);
56
+ }
57
+
58
+ const newScript = document.createElement("script");
59
+ newScript.dataset.quid = script.dataset.quid;
60
+ newScript.innerHTML = script.innerHTML;
61
+ document.head.appendChild(newScript);
62
+ break;
63
+ case "replace":
64
+ element.outerHTML = data;
65
+ break;
66
+ default:
67
+ throw `mode ${mode} is not valid`;
68
+ }
63
69
  }
64
70
  }).catch(err => {
65
71
  if (!handleErrors) throw err;
metadata CHANGED
@@ -1,15 +1,57 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: quince
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joseph Johansen
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-10-15 00:00:00.000000000 Z
11
+ date: 2021-10-17 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: sinatra
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: sinatra-contrib
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rack-contrib
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.3'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.3'
13
55
  - !ruby/object:Gem::Dependency
14
56
  name: typed_struct
15
57
  requirement: !ruby/object:Gem::Requirement
@@ -62,6 +104,7 @@ files:
62
104
  - lib/quince/component.rb
63
105
  - lib/quince/html_tag_components.rb
64
106
  - lib/quince/serialiser.rb
107
+ - lib/quince/sinatra.rb
65
108
  - lib/quince/singleton_methods.rb
66
109
  - lib/quince/types.rb
67
110
  - lib/quince/version.rb