unival 0.0.2 → 0.0.3

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
  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