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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6f131f2b231fa0981cc0dcdbf9a4c2c66e6a01c2
4
- data.tar.gz: f909669d667ae23c4126135c99373270cda9b2a6
3
+ metadata.gz: 0204bad560f1a43b5a28a93df8e85b043d5cd774
4
+ data.tar.gz: cb8513fbd798e2a01a63577c5b387a9f608a2198
5
5
  SHA512:
6
- metadata.gz: ab72aa119283e6b348bb7d1a0113f8dce488871e3f0eeba0cd8f62afd7ea85e464b5fb2fe7d310643dcc1c06ed5fea4d9c1b54b8ad8956b9f9fe1321e4e95308
7
- data.tar.gz: 0873880dde74259563b8e0498a9144f1d934baf7da48c6a632bce5652a824b96a2734f27d24e517822e6dcdf6bfb47c126f6f05f701378240c4742e2da181378
6
+ metadata.gz: 741ebc19adb0a168780646444b0c296752ee6f1181542a688ac31162a632b2cad07791f84fc7d8beff639a523b7a4561f2bb6201f4c158d7f9c8611f61fa1650
7
+ data.tar.gz: 6f6d53b4ac06a4c5fac071069085b4d0188012667b659a46cb8db44cf01756493b377729e87f1d77e0fed41f404a19569baa91d8084b35ee40376c57eeac4506
@@ -0,0 +1,9 @@
1
+ gemfile:
2
+ - gemfiles/Gemfile.rails-3.2.x
3
+ - gemfiles/Gemfile.rails-4.1.x
4
+ rvm:
5
+ - 2.0
6
+ - 2.1
7
+ - 2.2
8
+ cache: bundler
9
+ sudo: false
data/Gemfile CHANGED
@@ -6,7 +6,7 @@ source "http://rubygems.org"
6
6
  # $ BUNDLE_GEMFILE=gemfiles/Gemfile.rails-4.1.x bundle exec rake
7
7
  #
8
8
  # etc.
9
- gem 'activemodel', "> 3"
9
+ gem 'activemodel', "~> 3"
10
10
  gem 'rack', '~> 1.4'
11
11
 
12
12
  # Add dependencies to develop your gem here.
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
@@ -1,4 +1,5 @@
1
1
  module Unival
2
2
  require_relative 'unival/version'
3
+ require_relative 'unival/utils'
3
4
  require_relative 'unival/app'
4
5
  end
@@ -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
- model_class_name = query_params.delete('model')
21
- raise Inv, "No model class given (by default passed as the `model' query-string param)" unless model_class_name.present?
22
-
23
- model_class = Kernel.const_get(model_class_name)
24
- raise Inv, "Invalid model or model not permitted" unless model_accessible?(model_class)
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 model_class.respond_to?(:new)
28
- model_class.new
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 model_class.respond_to?(:find)
33
- model_class.find(model_id)
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
- model_data = if query_params['format'].to_s.downcase == 'jquery'
37
- repack_jquery_serialization(model_class_name, params)
38
- else
39
- params
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: model_class.to_s, is_create: is_create, valid: true, errors: nil}
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
- d = {model: model_class.to_s, is_create: is_create, valid: false, errors: model.errors.as_json}
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 === Inv
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
- d = {error: e.message}
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
- # Logs the exception for later use
77
- def log_exception(e)
78
- $stderr.puts e.message
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
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Unival
2
- VERSION = '0.0.2'
2
+ VERSION = '0.0.3'
3
3
  end
@@ -4,10 +4,42 @@ require 'rack/test'
4
4
  require 'active_record'
5
5
  require 'sqlite3'
6
6
 
7
- describe 'Unival full-stack' do
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
@@ -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 in raw JSON format for a new record and returns an error if the saving would cause a duplicate' do
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
 
@@ -4,5 +4,5 @@ require 'rspec'
4
4
  require 'unival'
5
5
 
6
6
  RSpec.configure do |config|
7
-
7
+ config.order = 'random'
8
8
  end
@@ -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.2 ruby lib
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.2"
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-19"
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>, [">= 0"])
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>, [">= 0"])
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>, [">= 0"])
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.2
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-19 00:00:00.000000000 Z
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: '0'
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: '0'
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