quince 0.5.1 โ†’ 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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