unival 0.0.2 → 0.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 +4 -4
- data/.travis.yml +9 -0
- data/Gemfile +1 -1
- data/README.md +9 -0
- data/gemfiles/Gemfile.rails-3.2.x +17 -0
- data/gemfiles/Gemfile.rails-4.1.x +17 -0
- data/lib/unival.rb +1 -0
- data/lib/unival/app.rb +33 -41
- data/lib/unival/utils.rb +19 -0
- data/lib/unival/version.rb +1 -1
- data/spec/app_spec.rb +59 -1
- data/spec/full_stack_spec.rb +3 -8
- data/spec/spec_helper.rb +1 -1
- data/unival.gemspec +10 -6
- metadata +10 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0204bad560f1a43b5a28a93df8e85b043d5cd774
|
4
|
+
data.tar.gz: cb8513fbd798e2a01a63577c5b387a9f608a2198
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 741ebc19adb0a168780646444b0c296752ee6f1181542a688ac31162a632b2cad07791f84fc7d8beff639a523b7a4561f2bb6201f4c158d7f9c8611f61fa1650
|
7
|
+
data.tar.gz: 6f6d53b4ac06a4c5fac071069085b4d0188012667b659a46cb8db44cf01756493b377729e87f1d77e0fed41f404a19569baa91d8084b35ee40376c57eeac4506
|
data/.travis.yml
ADDED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -36,6 +36,15 @@ full-on replace assignment).
|
|
36
36
|
|
37
37
|
will give the same response as the POST example, except that `is_create` will be set to `true`.
|
38
38
|
|
39
|
+
## Translations
|
40
|
+
|
41
|
+
If you have I18n enabled, and you have enabled translation introspection, you will receive the translation keys instead of the
|
42
|
+
translated error strings. To set up translation introspection, make sure that the I18n backend you use supports introspection:
|
43
|
+
|
44
|
+
class << I18n.backend
|
45
|
+
include I18n::Backend::Metadata
|
46
|
+
end
|
47
|
+
|
39
48
|
## Concerns
|
40
49
|
|
41
50
|
This approach is relatively insecure as it allows for probing. Use something like `Rack::Attack` to limit the number of
|
@@ -0,0 +1,17 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
|
3
|
+
gem 'activemodel', "> 3", '< 4'
|
4
|
+
gem 'rack', '~> 1.4'
|
5
|
+
|
6
|
+
# Add dependencies to develop your gem here.
|
7
|
+
# Include everything needed to run rake, tests, features, etc.
|
8
|
+
group :development do
|
9
|
+
gem 'activerecord', "~> 3", '< 4'
|
10
|
+
gem 'rack-test'
|
11
|
+
gem 'rake', '~> 10.0'
|
12
|
+
gem 'yard'
|
13
|
+
gem 'sqlite3'
|
14
|
+
gem "rspec", "~> 3.4"
|
15
|
+
gem "bundler", "~> 1.0"
|
16
|
+
gem "jeweler", '~> 2.1'
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
|
3
|
+
gem 'activemodel', "> 3"
|
4
|
+
gem 'rack', '~> 1.4'
|
5
|
+
|
6
|
+
# Add dependencies to develop your gem here.
|
7
|
+
# Include everything needed to run rake, tests, features, etc.
|
8
|
+
group :development do
|
9
|
+
gem 'activerecord', "~> 4"
|
10
|
+
gem 'rack-test'
|
11
|
+
gem 'rake', '~> 10.0'
|
12
|
+
gem 'yard'
|
13
|
+
gem 'sqlite3'
|
14
|
+
gem "rspec", "~> 3.4"
|
15
|
+
gem "bundler", "~> 1.0"
|
16
|
+
gem "jeweler", '~> 2.1'
|
17
|
+
end
|
data/lib/unival.rb
CHANGED
data/lib/unival/app.rb
CHANGED
@@ -1,9 +1,10 @@
|
|
1
1
|
require 'json'
|
2
2
|
|
3
3
|
class Unival::App
|
4
|
+
include Unival::Utils
|
5
|
+
|
4
6
|
SUPPORTED_METHODS = %w( PUT POST PATCH )
|
5
7
|
Inv = Class.new(StandardError)
|
6
|
-
|
7
8
|
def call(env)
|
8
9
|
req = Rack::Request.new(env)
|
9
10
|
|
@@ -17,27 +18,26 @@ class Unival::App
|
|
17
18
|
|
18
19
|
query_params = extract_query_or_route_params_from(req)
|
19
20
|
|
20
|
-
|
21
|
-
raise Inv, "No model class given (by default passed as the `model' query-string param)"
|
22
|
-
|
23
|
-
|
24
|
-
raise Inv, "Invalid model or model not permitted" unless model_accessible?(
|
21
|
+
model_module_name = query_params.delete('model')
|
22
|
+
raise Inv, "No model class given (by default passed as the `model' query-string param)" if model_module_name.to_s.empty?
|
23
|
+
|
24
|
+
model_module = Kernel.const_get(model_module_name)
|
25
|
+
raise Inv, "Invalid model or model not permitted" unless model_accessible?(model_module)
|
25
26
|
|
26
27
|
model = if req.post?
|
27
|
-
raise Inv, "The model module does not support .new" unless
|
28
|
-
|
28
|
+
raise Inv, "The model module does not support .new" unless model_module.respond_to?(:new)
|
29
|
+
model_module.new
|
29
30
|
else
|
30
31
|
model_id = query_params.delete('id')
|
31
32
|
raise Inv, "No model ID to find given (by default passed as the `id' query-string param)" unless model_id
|
32
|
-
raise Inv, "The model module does not support .find" unless
|
33
|
-
|
33
|
+
raise Inv, "The model module does not support .find" unless model_module.respond_to?(:find)
|
34
|
+
model_module.find(model_id)
|
34
35
|
end
|
35
36
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
end
|
37
|
+
# Instead of scanning for instance_methods, check the object itself.
|
38
|
+
raise Inv, "The model (#{model.class}) does not support `#valid?'" unless model.respond_to?(:valid?)
|
39
|
+
|
40
|
+
model_data = filter_model_params(model_module, params)
|
41
41
|
|
42
42
|
# Instead of scanning for instance_methods, check the object itself.
|
43
43
|
raise Inv, "The model does not support `#valid?'" unless model.respond_to?(:valid?)
|
@@ -47,23 +47,22 @@ class Unival::App
|
|
47
47
|
|
48
48
|
is_create = req.post?
|
49
49
|
if model.valid?
|
50
|
-
d = {model:
|
50
|
+
d = {model: model_module.to_s, is_create: is_create, valid: true, errors: nil}
|
51
51
|
[200, {'Content-Type' => 'application/json'}, [JSON.dump(d)]]
|
52
52
|
else
|
53
|
-
|
53
|
+
model_errors = replace_with_translation_keys(model.errors.as_json)
|
54
|
+
d = {model: model_module.to_s, is_create: is_create, valid: false, errors: model_errors}
|
54
55
|
[409, {'Content-Type' => 'application/json'}, [JSON.dump(d)]]
|
55
56
|
end
|
56
57
|
rescue Exception => e
|
57
58
|
if e.to_s =~ /NotFound/
|
58
59
|
d = {error: "Model not found: #{e}"}
|
59
60
|
[404, {'Content-Type' => 'application/json'}, [JSON.dump(d)]]
|
60
|
-
elsif e
|
61
|
+
elsif e.is_a?(Inv)
|
61
62
|
d = {error: e.message}
|
62
63
|
[400, {'Content-Type' => 'application/json'}, [JSON.dump(d)]]
|
63
|
-
[]
|
64
64
|
else
|
65
|
-
|
66
|
-
[502, {'Content-Type' => 'application/json'}, [JSON.dump(d)]]
|
65
|
+
raise e # Something we can't handle internally, raise it up the stack for eventual exception capture in middleware
|
67
66
|
end
|
68
67
|
end
|
69
68
|
|
@@ -73,30 +72,23 @@ class Unival::App
|
|
73
72
|
true
|
74
73
|
end
|
75
74
|
|
76
|
-
#
|
77
|
-
|
78
|
-
|
75
|
+
# Replaces the literal strings in the model errors (furnished as
|
76
|
+
# a json-able Hash with arbitrary nesting) with the I18n keys.
|
77
|
+
# Only gets performed if the translation introspection module is present
|
78
|
+
# on the I18n backend currently in use.
|
79
|
+
def replace_with_translation_keys(model_errors)
|
80
|
+
return model_errors if internationalized?
|
81
|
+
deep_translation_replace(model_errors)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Can be used to do optional parameter filtering.
|
85
|
+
# If you want to use strong parameters, this is the place to apply them.
|
86
|
+
def filter_model_params(model_module, params)
|
87
|
+
params
|
79
88
|
end
|
80
89
|
|
81
90
|
# Extract params like :format, :id and :model
|
82
91
|
def extract_query_or_route_params_from(rack_request)
|
83
92
|
Rack::Utils.parse_nested_query(rack_request.query_string)
|
84
93
|
end
|
85
|
-
|
86
|
-
# Hackishly use Rack to reconstruct a hash of keys-values, with nesting
|
87
|
-
def repack_jquery_serialization(model_class_name, jquery_array_of_fields)
|
88
|
-
reassembled_query = jquery_array_of_fields.map do |elem|
|
89
|
-
Rack::Utils.escape(elem.fetch('name')) + '=' + Rack::Utils.escape(elem.fetch('value'))
|
90
|
-
end.join('&')
|
91
|
-
|
92
|
-
param_hash = Rack::Utils.parse_nested_query(reassembled_query)
|
93
|
-
|
94
|
-
raise "The resulting parametric object must be a Hash" unless param_hash.is_a?(Hash)
|
95
|
-
raise "The resulting parametric object must have 1 key" unless param_hash.keys.one?
|
96
|
-
object_params = param_hash.fetch(param_hash.keys[0])
|
97
|
-
|
98
|
-
raise "The resulting unwrapped object params must be a Hash" unless object_params.is_a?(Hash)
|
99
|
-
|
100
|
-
return object_params.with_indifferent_access
|
101
|
-
end
|
102
94
|
end
|
data/lib/unival/utils.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
module Unival::Utils
|
2
|
+
def deep_translation_replace(v)
|
3
|
+
if v.is_a?(Array)
|
4
|
+
v.map{|e| deep_translation_replace(e) }
|
5
|
+
elsif v.is_a?(Hash)
|
6
|
+
v.each_with_object({}){|(k, v), o| o[deep_translation_replace(k)] = deep_translation_replace(v) }
|
7
|
+
else
|
8
|
+
if v.respond_to?(:translation_metadata)
|
9
|
+
v.translation_metadata.fetch(:key)
|
10
|
+
else
|
11
|
+
v
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def internationalized?
|
17
|
+
!defined?(I18n) || !I18n.backend
|
18
|
+
end
|
19
|
+
end
|
data/lib/unival/version.rb
CHANGED
data/spec/app_spec.rb
CHANGED
@@ -4,10 +4,42 @@ require 'rack/test'
|
|
4
4
|
require 'active_record'
|
5
5
|
require 'sqlite3'
|
6
6
|
|
7
|
-
describe 'Unival
|
7
|
+
describe 'Unival app' do
|
8
8
|
include Rack::Test::Methods
|
9
9
|
let(:app) { Unival::App.new }
|
10
10
|
|
11
|
+
describe 'with a model module that supports the proper methods' do
|
12
|
+
module SomeModel
|
13
|
+
class UserData < Struct.new(:name)
|
14
|
+
def valid?
|
15
|
+
name == 'John'
|
16
|
+
end
|
17
|
+
|
18
|
+
def errors
|
19
|
+
['name is wrong']
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.find(id)
|
24
|
+
return SomeModel
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe 'with a model module that returns nil from find()' do
|
30
|
+
module NilReturningModel
|
31
|
+
def self.find(id); end
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'returns a 400 and explains what happened' do
|
35
|
+
put '/?id=123&model=NilReturningModel', JSON.dump({name: 'Julik', email: 'julik@example.com'})
|
36
|
+
|
37
|
+
expect(last_response).not_to be_ok
|
38
|
+
parsed = JSON.parse(last_response.body, symbolize_names: true)
|
39
|
+
expect(parsed).to eq({:error=>"The model (NilClass) does not support `#valid?'"})
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
11
43
|
describe 'with a class that gives a nonsensical to_json' do
|
12
44
|
class Nonsense
|
13
45
|
def self.to_json(encoder)
|
@@ -31,4 +63,30 @@ describe 'Unival full-stack' do
|
|
31
63
|
expect(parsed).to eq({:model => "Nonsense", :is_create => true, :valid => true, :errors => nil})
|
32
64
|
end
|
33
65
|
end
|
66
|
+
|
67
|
+
describe 'with enabled I18n that provides introspection' do
|
68
|
+
it 'replaces errors with their keys' do
|
69
|
+
fake_errors = [
|
70
|
+
double('missing field error string', translation_metadata: {key: 'missing.field'})
|
71
|
+
]
|
72
|
+
|
73
|
+
module TranslatedModel
|
74
|
+
end
|
75
|
+
|
76
|
+
fake_model = double('Translated model with errors')
|
77
|
+
|
78
|
+
expect(TranslatedModel).to receive(:new).and_return(fake_model)
|
79
|
+
|
80
|
+
expect(fake_model).to receive(:attributes=)
|
81
|
+
expect(fake_model).to receive(:valid?).and_return(false)
|
82
|
+
expect(fake_model).to receive(:errors).and_return(fake_errors)
|
83
|
+
expect(fake_errors).to receive(:as_json).and_return(fake_errors)
|
84
|
+
|
85
|
+
post '/?model=TranslatedModel', JSON.dump({name: 'Julik', email: 'julik@example.com'})
|
86
|
+
|
87
|
+
parsed = JSON.parse(last_response.body, symbolize_names: true)
|
88
|
+
expect(last_response).not_to be_ok
|
89
|
+
expect(parsed[:errors]).to eq(["missing.field"])
|
90
|
+
end
|
91
|
+
end
|
34
92
|
end
|
data/spec/full_stack_spec.rb
CHANGED
@@ -10,12 +10,7 @@ describe 'Unival full-stack' do
|
|
10
10
|
|
11
11
|
# This is a full-stack speck, so we are going to do real ActiveRecord and stuff.
|
12
12
|
before :all do
|
13
|
-
|
14
|
-
if ActiveRecord::VERSION::MAJOR < 4
|
15
|
-
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', dbfile: ':memory:')
|
16
|
-
else
|
17
|
-
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
|
18
|
-
end
|
13
|
+
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
|
19
14
|
|
20
15
|
ActiveRecord::Schema.define do
|
21
16
|
create_table :people do |t|
|
@@ -56,8 +51,8 @@ describe 'Unival full-stack' do
|
|
56
51
|
john = Person.create name: 'John', email: 'john@example.com'
|
57
52
|
|
58
53
|
post '/?model=Person', JSON.dump({name: 'John', email: 'another-john@example.com'})
|
59
|
-
parsed = JSON.parse(last_response.body, symbolize_names: true)
|
60
54
|
|
55
|
+
parsed = JSON.parse(last_response.body, symbolize_names: true)
|
61
56
|
expect(last_response).not_to be_ok
|
62
57
|
expect(last_response.status).to eq(409)
|
63
58
|
expect(parsed).to eq({
|
@@ -97,7 +92,7 @@ describe 'Unival full-stack' do
|
|
97
92
|
|
98
93
|
let(:app) { MoreRestrictive.new }
|
99
94
|
|
100
|
-
it 'performs validation
|
95
|
+
it 'performs validation for a model that is permitted, but forbids it for a model that is not' do
|
101
96
|
post '/?model=Person', JSON.dump({name: 'John', email: 'john@example.com'})
|
102
97
|
expect(last_response).to be_ok
|
103
98
|
|
data/spec/spec_helper.rb
CHANGED
data/unival.gemspec
CHANGED
@@ -2,16 +2,16 @@
|
|
2
2
|
# DO NOT EDIT THIS FILE DIRECTLY
|
3
3
|
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
4
|
# -*- encoding: utf-8 -*-
|
5
|
-
# stub: unival 0.0.
|
5
|
+
# stub: unival 0.0.3 ruby lib
|
6
6
|
|
7
7
|
Gem::Specification.new do |s|
|
8
8
|
s.name = "unival"
|
9
|
-
s.version = "0.0.
|
9
|
+
s.version = "0.0.3"
|
10
10
|
|
11
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
12
12
|
s.require_paths = ["lib"]
|
13
13
|
s.authors = ["Julik Tarkhanov"]
|
14
|
-
s.date = "2016-05-
|
14
|
+
s.date = "2016-05-23"
|
15
15
|
s.description = " A minimal endpoint for driving server-side validations from a remote UI "
|
16
16
|
s.email = "me@julik.nl"
|
17
17
|
s.extra_rdoc_files = [
|
@@ -21,12 +21,16 @@ Gem::Specification.new do |s|
|
|
21
21
|
s.files = [
|
22
22
|
".document",
|
23
23
|
".rspec",
|
24
|
+
".travis.yml",
|
24
25
|
"Gemfile",
|
25
26
|
"LICENSE.txt",
|
26
27
|
"README.md",
|
27
28
|
"Rakefile",
|
29
|
+
"gemfiles/Gemfile.rails-3.2.x",
|
30
|
+
"gemfiles/Gemfile.rails-4.1.x",
|
28
31
|
"lib/unival.rb",
|
29
32
|
"lib/unival/app.rb",
|
33
|
+
"lib/unival/utils.rb",
|
30
34
|
"lib/unival/version.rb",
|
31
35
|
"spec/app_spec.rb",
|
32
36
|
"spec/full_stack_spec.rb",
|
@@ -45,7 +49,7 @@ Gem::Specification.new do |s|
|
|
45
49
|
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
46
50
|
s.add_runtime_dependency(%q<activemodel>, ["> 3"])
|
47
51
|
s.add_runtime_dependency(%q<rack>, ["~> 1.4"])
|
48
|
-
s.add_development_dependency(%q<activerecord>, ["
|
52
|
+
s.add_development_dependency(%q<activerecord>, ["~> 4"])
|
49
53
|
s.add_development_dependency(%q<rack-test>, [">= 0"])
|
50
54
|
s.add_development_dependency(%q<rake>, ["~> 10.0"])
|
51
55
|
s.add_development_dependency(%q<yard>, [">= 0"])
|
@@ -56,7 +60,7 @@ Gem::Specification.new do |s|
|
|
56
60
|
else
|
57
61
|
s.add_dependency(%q<activemodel>, ["> 3"])
|
58
62
|
s.add_dependency(%q<rack>, ["~> 1.4"])
|
59
|
-
s.add_dependency(%q<activerecord>, ["
|
63
|
+
s.add_dependency(%q<activerecord>, ["~> 4"])
|
60
64
|
s.add_dependency(%q<rack-test>, [">= 0"])
|
61
65
|
s.add_dependency(%q<rake>, ["~> 10.0"])
|
62
66
|
s.add_dependency(%q<yard>, [">= 0"])
|
@@ -68,7 +72,7 @@ Gem::Specification.new do |s|
|
|
68
72
|
else
|
69
73
|
s.add_dependency(%q<activemodel>, ["> 3"])
|
70
74
|
s.add_dependency(%q<rack>, ["~> 1.4"])
|
71
|
-
s.add_dependency(%q<activerecord>, ["
|
75
|
+
s.add_dependency(%q<activerecord>, ["~> 4"])
|
72
76
|
s.add_dependency(%q<rack-test>, [">= 0"])
|
73
77
|
s.add_dependency(%q<rake>, ["~> 10.0"])
|
74
78
|
s.add_dependency(%q<yard>, [">= 0"])
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: unival
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Julik Tarkhanov
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-05-
|
11
|
+
date: 2016-05-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|
@@ -42,16 +42,16 @@ dependencies:
|
|
42
42
|
name: activerecord
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- - "
|
45
|
+
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
47
|
+
version: '4'
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- - "
|
52
|
+
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: '
|
54
|
+
version: '4'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: rack-test
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -161,12 +161,16 @@ extra_rdoc_files:
|
|
161
161
|
files:
|
162
162
|
- ".document"
|
163
163
|
- ".rspec"
|
164
|
+
- ".travis.yml"
|
164
165
|
- Gemfile
|
165
166
|
- LICENSE.txt
|
166
167
|
- README.md
|
167
168
|
- Rakefile
|
169
|
+
- gemfiles/Gemfile.rails-3.2.x
|
170
|
+
- gemfiles/Gemfile.rails-4.1.x
|
168
171
|
- lib/unival.rb
|
169
172
|
- lib/unival/app.rb
|
173
|
+
- lib/unival/utils.rb
|
170
174
|
- lib/unival/version.rb
|
171
175
|
- spec/app_spec.rb
|
172
176
|
- spec/full_stack_spec.rb
|