strelka-presenters 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: bde111f87ede9477a436d489cf6b8675ab93031fb3231a08965c227389c59aa3
4
+ data.tar.gz: b64413df84cec398d8f5c45a22fca13c09deb5caa73238ca360ca5fd68d88869
5
+ SHA512:
6
+ metadata.gz: bb85149d0e58a0ed2fc38b6b675a955315933d4d67885044c5e07a785621b80e0f818f766aa30bbadf13c511dd5baf640bd77588c96ce0456076e5719369e1d5
7
+ data.tar.gz: 0304763de7b1e4f01b2f64cfb618e968af67780843038d28b876ba671777d44d354a914dee477b750e1866726a969bc28ec665126c6cdb561ce222e9251a6052
Binary file
@@ -0,0 +1,4 @@
1
+ ��Cʥ�Fe؈��|�����znk��vwn3����!��
2
+ zG+HA��Pi���Ѝhewu�T�l���vX`&��^�d32])�?J(� ��s�!x�cS�+vٳ,,O
3
+ ���w0�q�*N�w�8{�n{5w�3�Aޑ����5op/�6�摰qZ&��lv屹�[��u�0 &>�$W_Z�G4^#�y,��St&^0�b=B���7P}[; 6��DZ!MD��<�G1�M4i��e��j~�P��2�%����]Z6�6?V���B(wq����y�h٩�s�������!Ѽ��N���G�5��O����w{89X��vbfyGm�?#:Չ;��)
4
+ �g���"hk%���K�0����9����G]Nеi������
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ README.md
3
+ ChangeLog.md
4
+
5
+ LICENSE.txt
@@ -0,0 +1,16 @@
1
+ --- !ruby/object:RDoc::Options
2
+ encoding: UTF-8
3
+ static_path: []
4
+ rdoc_include:
5
+ - .
6
+ charset: UTF-8
7
+ exclude:
8
+ hyperlink_all: false
9
+ line_numbers: false
10
+ main_page: README.md
11
+ markup: markdown
12
+ show_hash: false
13
+ tab_width: 8
14
+ title: strelka-Presenters Documentation
15
+ visibility: :protected
16
+ webcvs:
@@ -0,0 +1,9 @@
1
+ # Simplecov config
2
+
3
+ SimpleCov.start do
4
+ add_filter 'spec'
5
+ add_filter 'integration'
6
+ add_group "Needing tests" do |file|
7
+ file.covered_percent < 90
8
+ end
9
+ end
@@ -0,0 +1,42 @@
1
+ 2019-08-13 Michael Granger <ged@FaerieMUD.org>
2
+
3
+ @ * README.md:
4
+ | Fix URLs and title in the README
5
+ | [6ac7de9075da] [tip]
6
+ |
7
+ 2019-08-12 Michael Granger <ged@FaerieMUD.org>
8
+
9
+ o * Rakefile, strelka-presenters.gemspec:
10
+ | Add URLs to the gemspec.
11
+ | [720bbedd3323]
12
+ |
13
+ o * strelka-presenters.gemspec:
14
+ | Commit the pre-release gemspec
15
+ | [7193b0e574d3]
16
+ |
17
+ 2019-08-11 Michael Granger <ged@FaerieMUD.org>
18
+
19
+ o * .hgignore, Manifest.txt:
20
+ | Update the manifest
21
+ | [dfa3077cfaeb]
22
+ |
23
+ o * Rakefile:
24
+ | Remove the publishing guard
25
+ | [3d2b12a06693]
26
+ |
27
+ 2019-08-07 Michael Granger <ged@FaerieMUD.org>
28
+
29
+ o * .document, .editorconfig, .gems, .hgignore, .pryrc, .rdoc_options,
30
+ .ruby-gemset, .ruby-version, .simplecov, Gemfile, History.md,
31
+ LICENSE.txt, Manifest.txt, README.md, Rakefile, certs/ged.pem,
32
+ lib/strelka/app/presenters.rb,
33
+ lib/strelka/httpresponse/presenters.rb, lib/strelka/presenters.rb,
34
+ spec/spec_helper.rb, spec/strelka/app/presenters_spec.rb,
35
+ spec/strelka/httpresponse/presenters_spec.rb,
36
+ spec/strelka/presenters_spec.rb, spec/testlib.rb,
37
+ spec/testlib/base.rb, spec/testlib/fixtures.rb,
38
+ spec/testlib/fixtures/groups.rb, spec/testlib/fixtures/users.rb,
39
+ spec/testlib/group.rb, spec/testlib/presenters.rb,
40
+ spec/testlib/user.rb:
41
+ Initial commit.
42
+ [83ab22adf1b7]
@@ -0,0 +1,4 @@
1
+ ## v0.1.0 [2019-08-24] Michael Granger <ged@FaerieMUD.org>
2
+
3
+ Initial release.
4
+
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2019 Michael Granger
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,24 @@
1
+ .document
2
+ .rdoc_options
3
+ .simplecov
4
+ ChangeLog
5
+ History.md
6
+ LICENSE.txt
7
+ Manifest.txt
8
+ README.md
9
+ Rakefile
10
+ lib/strelka/app/presenters.rb
11
+ lib/strelka/httpresponse/presenters.rb
12
+ lib/strelka/presenters.rb
13
+ spec/spec_helper.rb
14
+ spec/strelka/app/presenters_spec.rb
15
+ spec/strelka/httpresponse/presenters_spec.rb
16
+ spec/strelka/presenters_spec.rb
17
+ spec/testlib.rb
18
+ spec/testlib/base.rb
19
+ spec/testlib/fixtures.rb
20
+ spec/testlib/fixtures/groups.rb
21
+ spec/testlib/fixtures/users.rb
22
+ spec/testlib/group.rb
23
+ spec/testlib/presenters.rb
24
+ spec/testlib/user.rb
@@ -0,0 +1,107 @@
1
+ # Strelka-Presenters
2
+
3
+ home
4
+ : http://deveiate.org/projects/strelka-presenters
5
+
6
+ code
7
+ : http://bitbucket.org/ged/strelka-presenters
8
+
9
+ github
10
+ : https://github.com/ged/strelka-presenters
11
+
12
+ docs
13
+ : http://deveiate.org/code/strelka-presenters
14
+
15
+
16
+ ## Description
17
+
18
+ Strelka-Presenters is a plugin for the Strelka web framework that adds
19
+ integration with the [Yaks hypermedia library][Yaks].
20
+
21
+ ### Usage
22
+
23
+ Load the plugin in your Strelka app, passing the Yaks configuration block:
24
+
25
+ class Example::App < Strelka::App
26
+
27
+ plugin :presenters do
28
+ default_format :hal
29
+ rel_template 'https://example.com/rels/{rel}'
30
+ mapper_namespace Example::Service::Presenters
31
+ end
32
+
33
+ plugin :router
34
+
35
+ get 'users' do |request|
36
+ users = Example::Users.all
37
+ response = request.response
38
+ response.present( users )
39
+ return response
40
+ end
41
+
42
+ end
43
+
44
+ Your app will automatically load the `negotiation` plugin, and responses will have a #present method that will map objects passed to it using Yaks into an acceptable response format using HTTP content negotiation.
45
+
46
+ See the documentation for [Yaks][] for more info.
47
+
48
+ ## Prerequisites
49
+
50
+ * Ruby
51
+
52
+
53
+ ## Installation
54
+
55
+ $ gem install strelka-presenters
56
+
57
+
58
+ ## Contributing
59
+
60
+ You can check out the current development source with Mercurial via its
61
+ [project page][project]. Or if you prefer Git, via
62
+ [its Github mirror][gitmirror].
63
+
64
+ After checking out the source, run:
65
+
66
+ $ rake newb
67
+
68
+ This task will install any missing dependencies, run the tests/specs,
69
+ and generate the API documentation.
70
+
71
+
72
+ ## License
73
+
74
+ Copyright (c) 2019, Michael Granger
75
+ All rights reserved.
76
+
77
+ Redistribution and use in source and binary forms, with or without
78
+ modification, are permitted provided that the following conditions are met:
79
+
80
+ * Redistributions of source code must retain the above copyright notice,
81
+ this list of conditions and the following disclaimer.
82
+
83
+ * Redistributions in binary form must reproduce the above copyright notice,
84
+ this list of conditions and the following disclaimer in the documentation
85
+ and/or other materials provided with the distribution.
86
+
87
+ * Neither the name of the author/s, nor the names of the project's
88
+ contributors may be used to endorse or promote products derived from this
89
+ software without specific prior written permission.
90
+
91
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
92
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
93
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
94
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
95
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
96
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
97
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
98
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
99
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
100
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
101
+
102
+
103
+
104
+ [project]: http://bitbucket.org/ged/strelka-presenters
105
+ [Yaks]: https://github.com/plexus/yaks/blob/master/yaks/README.md
106
+ [gitmirror]: https://github.com/ged/strelka-presenters
107
+
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env rake
2
+
3
+ begin
4
+ require 'hoe'
5
+ rescue LoadError
6
+ abort "This Rakefile requires hoe (gem install hoe)"
7
+ end
8
+
9
+ GEMSPEC = 'strelka-presenters.gemspec'
10
+
11
+
12
+ Hoe.plugin :mercurial
13
+ Hoe.plugin :signing
14
+ Hoe.plugin :deveiate
15
+
16
+ Hoe.plugins.delete :rubyforge
17
+
18
+ hoespec = Hoe.spec 'strelka-presenters' do |spec|
19
+
20
+ spec.readme_file = 'README.md'
21
+ spec.history_file = 'History.md'
22
+
23
+ spec.extra_rdoc_files = FileList[ '*.rdoc', '*.md' ]
24
+ spec.license 'BSD-3-Clause'
25
+ spec.urls = {
26
+ home: 'https://bitbucket.org/ged/strelka-presenters',
27
+ code: 'https://bitbucket.org/ged/strelka-presenters/src',
28
+ docs: 'https://deveiate.org/code/strelka-presenters',
29
+ github: 'https://github.com/ged/strelka-presenters',
30
+ }
31
+
32
+ spec.developer 'Michael Granger', 'ged@FaerieMUD.org'
33
+
34
+ spec.dependency 'strelka', '~> 0.16'
35
+ spec.dependency 'yaks', '~> 0.13'
36
+
37
+ spec.dependency 'faker', '~> 2.1'
38
+ spec.dependency 'fluent_fixtures', '~> 0.8'
39
+ spec.dependency 'hoe-deveiate', '~> 0.10', :developer
40
+ spec.dependency 'simplecov', '~> 0.17', :developer
41
+ spec.dependency 'rdoc-generator-fivefish', '~> 0.1', :developer
42
+
43
+ spec.require_ruby_version( '>=2.5.0' )
44
+ spec.hg_sign_tags = true if spec.respond_to?( :hg_sign_tags= )
45
+ spec.check_history_on_release = true if spec.respond_to?( :check_history_on_release= )
46
+
47
+ self.rdoc_locations << "deveiate:/usr/local/www/public/code/#{remote_rdoc_dir}"
48
+ end
49
+
50
+
51
+ ENV['VERSION'] ||= hoespec.spec.version.to_s
52
+
53
+ # Run the tests before checking in
54
+ task 'hg:precheckin' => [ :check_history, :check_manifest, :gemspec, :spec ]
55
+
56
+ task :test => :spec
57
+
58
+ # Rebuild the ChangeLog immediately before release
59
+ task :prerelease => 'ChangeLog'
60
+ CLOBBER.include( 'ChangeLog' )
61
+
62
+ desc "Build a coverage report"
63
+ task :coverage do
64
+ ENV["COVERAGE"] = 'yes'
65
+ Rake::Task[:spec].invoke
66
+ end
67
+ CLOBBER.include( 'coverage' )
68
+
69
+
70
+ # Use the fivefish formatter for docs generated from development checkout
71
+ if File.directory?( '.hg' )
72
+ require 'rdoc/task'
73
+
74
+ Rake::Task[ 'docs' ].clear
75
+ RDoc::Task.new( 'docs' ) do |rdoc|
76
+ rdoc.main = "README.md"
77
+ rdoc.markup = 'markdown'
78
+ rdoc.rdoc_files.include( "*.md", "ChangeLog", "lib/**/*.rb" )
79
+ rdoc.generator = :fivefish
80
+ rdoc.title = 'Strelka Presenters Plugin'
81
+ rdoc.rdoc_dir = 'doc'
82
+ end
83
+ end
84
+
85
+ task :gemspec => GEMSPEC
86
+ file GEMSPEC => __FILE__
87
+ task GEMSPEC do |task|
88
+ spec = $hoespec.spec
89
+ spec.files.delete( '.gemtest' )
90
+ spec.signing_key = nil
91
+ spec.cert_chain = ['certs/ged.pem']
92
+ spec.version = "#{spec.version.bump}.0.pre#{Time.now.strftime("%Y%m%d%H%M%S")}"
93
+ File.open( task.name, 'w' ) do |fh|
94
+ fh.write( spec.to_ruby )
95
+ end
96
+ end
97
+ CLOBBER.include( GEMSPEC.to_s )
98
+
99
+ task :default => :gemspec
100
+
@@ -0,0 +1,85 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require 'yaks'
5
+
6
+ require 'strelka/app' unless defined?( Strelka::App )
7
+ require 'strelka/plugins'
8
+ require 'strelka/mixins'
9
+
10
+ require 'strelka/httpresponse/presenters'
11
+
12
+
13
+ # Presentation layer plugin for Strelka service apps.
14
+ module Strelka::App::Presenters
15
+ include Strelka::Constants
16
+ extend Strelka::Plugin
17
+
18
+ run_outside :routing, :filters
19
+ run_inside :templating, :parameters, :negotiation
20
+
21
+
22
+ # Class methods to add to classes with presenters
23
+ module ClassMethods
24
+
25
+ ##
26
+ # The block given to Yaks when it's configured
27
+ attr_accessor :presenter_config_block
28
+
29
+ end # module ClassMethods
30
+
31
+
32
+ ### Extension callback -- extend the HTTPResponse classes with presenter logic
33
+ ### when the plugin is loaded.
34
+ def self::included( object )
35
+ Strelka::HTTPResponse.include( Strelka::HTTPResponse::Presenters )
36
+ super
37
+ end
38
+
39
+
40
+ ### Set a block passed to `plugin` to the presenter library when it's
41
+ ### configured.
42
+ def self::configure_block( app, &block )
43
+ app.plugin( :negotiation )
44
+ app.presenter_config_block = block
45
+ end
46
+
47
+
48
+ ### Set up the presenters when the app is created.
49
+ def initialize( * )
50
+ super
51
+ @presenter = self.setup_presentation_layer
52
+ end
53
+
54
+
55
+ ######
56
+ public
57
+ ######
58
+
59
+ ##
60
+ # The configured Yaks presenter object
61
+ attr_reader :presenter
62
+
63
+
64
+ ### Set up the presenter when the response has returned.
65
+ def handle_request( req )
66
+ self.log.debug "[:negotiation] Wrapping response with hypermedia presentation."
67
+
68
+ response = super
69
+ response.presenter = self.presenter
70
+
71
+ return response
72
+ end
73
+
74
+
75
+ ### Configure and return the presenter.
76
+ def setup_presentation_layer
77
+ self.log.debug "Setting up presenters using %s" %
78
+ [ self.class.presenter_config_block&.inspect || 'defaults' ]
79
+ config_block = self.class.presenter_config_block || Proc.new {}
80
+ return Yaks.new( &config_block )
81
+ end
82
+
83
+ end # module Strelka::App::Presenters
84
+
85
+
@@ -0,0 +1,45 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require 'yaks'
5
+
6
+ require 'strelka/httpresponse' unless defined?( Strelka::HTTPResponse )
7
+
8
+
9
+ # Hypermedia presentation logic for Strelka HTTP responses
10
+ module Strelka::HTTPResponse::Presenters
11
+
12
+ ### Initialize some instance variables when a new response is created.
13
+ def initialize( * )
14
+ @presenter = nil
15
+ super
16
+ end
17
+
18
+
19
+ ######
20
+ public
21
+ ######
22
+
23
+ ##
24
+ # The presenter to use when negotiating the response
25
+ attr_accessor :presenter
26
+
27
+
28
+ ### Added so we can pass the response object to the presenters as the `env`.
29
+ def []( * ) # :nodoc:
30
+ return nil
31
+ end
32
+
33
+
34
+ ### Set the entity that should be rendered to form the response body.
35
+ def present( entity, **options )
36
+ Yaks::Format.all.each do |format|
37
+ self.for( format.media_type ) do
38
+ opts = options.merge( format: format.format_name, env: self )
39
+ self.presenter.call( entity, opts )
40
+ end
41
+ end
42
+ end
43
+
44
+ end # module Strelka::HTTPResponse::Presenters
45
+
@@ -0,0 +1,21 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require 'strelka' unless defined?( Strelka )
5
+
6
+
7
+ # Toplevel namespace
8
+ module Strelka::Presenters
9
+
10
+ # Package version
11
+ VERSION = '0.1.0'
12
+
13
+ # Version control revision
14
+ REVISION = %q$Revision$
15
+
16
+
17
+ require 'strelka/app/presenters'
18
+ require 'strelka/httpresponse/presenters'
19
+
20
+ end # module Strelka::Presenters
21
+
@@ -0,0 +1,42 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ BEGIN {
5
+ $LOAD_PATH.unshift( '../Strelka/lib' )
6
+ }
7
+
8
+ require 'simplecov' if ENV['COVERAGE']
9
+
10
+ require 'rspec'
11
+
12
+ require 'loggability/spechelpers'
13
+ require 'mongrel2'
14
+ require 'mongrel2/testing'
15
+ require 'strelka'
16
+ require 'strelka/testing'
17
+
18
+
19
+ ### Mock with RSpec
20
+ RSpec.configure do |config|
21
+ config.mock_with( :rspec ) do |mock|
22
+ mock.syntax = :expect
23
+ end
24
+
25
+ config.disable_monkey_patching!
26
+ config.example_status_persistence_file_path = "spec/.status"
27
+ config.filter_run :focus
28
+ config.filter_run_when_matching :focus
29
+ config.order = :random
30
+ config.profile_examples = 5
31
+ config.run_all_when_everything_filtered = true
32
+ config.shared_context_metadata_behavior = :apply_to_host_groups
33
+ # config.warnings = true
34
+
35
+ config.include( Loggability::SpecHelpers )
36
+ config.include( Mongrel2::SpecHelpers )
37
+ config.include( Mongrel2::Constants )
38
+ config.include( Strelka::Constants )
39
+ config.include( Strelka::Testing )
40
+ end
41
+
42
+
@@ -0,0 +1,260 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../../spec_helper'
5
+
6
+ require 'strelka'
7
+ require 'mongrel2/testing'
8
+ require 'strelka/testing'
9
+ require 'strelka/constants'
10
+ require 'strelka/behavior/plugin'
11
+
12
+ require 'testlib'
13
+
14
+ require 'strelka/app/presenters'
15
+
16
+
17
+ RSpec.describe( Strelka::App::Presenters ) do
18
+
19
+ TEST_SEND_SPEC = 'tcp://127.0.0.1:12012'
20
+ TEST_RECV_SPEC = 'tcp://127.0.0.1:12013'
21
+
22
+
23
+ before( :all ) do
24
+ Testlib::Fixtures.load( :users, :groups )
25
+
26
+ @request_factory = Mongrel2::RequestFactory.new(
27
+ route: '/api/v1',
28
+ host: 'acme.com',
29
+ port: 80
30
+ )
31
+ end
32
+
33
+
34
+ it_should_behave_like( "A Strelka Plugin" )
35
+
36
+
37
+ describe "an including App" do
38
+
39
+ before( :each ) do
40
+ Testlib.reset
41
+
42
+ @app = Class.new( Strelka::App ) do
43
+ def initialize( appid='presenters-test', sspec=TEST_SEND_SPEC, rspec=TEST_RECV_SPEC )
44
+ super
45
+ end
46
+
47
+ plugins :routing
48
+ router :exclusive
49
+
50
+ plugin :parameters
51
+ param :id, :integer
52
+
53
+ plugin :presenters do
54
+ default_format :hal
55
+ rel_template 'https://acme.com/rels/{rel}'
56
+ format_options( :hal, plural_links: [:copyright] )
57
+ mapper_namespace Testlib::Presenters
58
+ end
59
+
60
+ get '/users' do |req|
61
+ res = req.response
62
+ res.present( Testlib::User.all )
63
+ return res
64
+ end
65
+
66
+ get '/users/:id' do |req|
67
+ id = req.params[:id]
68
+
69
+ user = Testlib::User[ id ] or finish_with HTTP::NOT_FOUND, "no such user"
70
+ res = req.response
71
+ res.present( user )
72
+
73
+ return res
74
+ end
75
+ end
76
+ end
77
+
78
+
79
+ let( :user_factory ) { Testlib::Fixtures.user.full }
80
+ let( :user_generator ) { user_factory.generator(create: true) }
81
+
82
+
83
+ it "can render a singleton HAL response" do
84
+ user = user_factory.create
85
+
86
+ req = @request_factory.get( "/api/v1/users/#{user.id}", accept: 'application/hal+json' )
87
+ res = @app.new.handle( req )
88
+
89
+ expect( res.status ).to eq( 200 )
90
+ expect( res.content_type ).to eq( 'application/hal+json' )
91
+ expect( res ).to have_json_body( Object ).
92
+ that_includes(
93
+ id: user.id,
94
+ name: user.name,
95
+ email: user.email,
96
+ login: user.login,
97
+ _links: a_hash_including(
98
+ self: {
99
+ href: "/api/v1/users/#{user.id}"
100
+ }
101
+ )
102
+ )
103
+ end
104
+
105
+
106
+ it "can render a collection HAL response" do
107
+ users = user_generator.take( 10 )
108
+
109
+ req = @request_factory.get( "/api/v1/users", accept: 'application/hal+json' )
110
+ res = @app.new.handle( req )
111
+
112
+ expect( res.status ).to eq( 200 )
113
+ expect( res.content_type ).to eq( 'application/hal+json' )
114
+ expect( res ).to have_json_body( Object ).
115
+ that_includes(
116
+ _embedded: {
117
+ "https://acme.com/rels/users": a_collection_containing_exactly(
118
+ *users.map {|u| a_hash_including(id: u.id) }
119
+ )
120
+ }
121
+ )
122
+ end
123
+
124
+
125
+ it "can render a singleton HALO response" do
126
+ user = user_factory.create
127
+
128
+ req = @request_factory.get( "/api/v1/users/#{user.id}", accept: 'application/halo+json' )
129
+ res = @app.new.handle( req )
130
+
131
+ expect( res.status ).to eq( 200 )
132
+ expect( res.content_type ).to eq( 'application/halo+json' )
133
+ expect( res ).to have_json_body( Object ).
134
+ that_includes(
135
+ id: user.id,
136
+ name: user.name,
137
+ email: user.email,
138
+ login: user.login,
139
+ _links: a_hash_including(
140
+ self: {
141
+ href: "/api/v1/users/#{user.id}"
142
+ }
143
+ )
144
+ )
145
+ end
146
+
147
+
148
+ it "can render a collection HALO response" do
149
+ users = user_generator.take( 10 )
150
+
151
+ req = @request_factory.get( "/api/v1/users", accept: 'application/halo+json' )
152
+ res = @app.new.handle( req )
153
+
154
+ expect( res.status ).to eq( 200 )
155
+ expect( res.content_type ).to eq( 'application/halo+json' )
156
+ expect( res ).to have_json_body( Object ).
157
+ that_includes(
158
+ _embedded: {
159
+ "https://acme.com/rels/users": a_collection_containing_exactly(
160
+ *users.map {|u| a_hash_including(id: u.id) }
161
+ )
162
+ }
163
+ )
164
+ end
165
+
166
+
167
+ it "can render a singleton JSON-API response" do
168
+ user = user_factory.create
169
+
170
+ req = @request_factory.get( "/api/v1/users/#{user.id}", accept: 'application/vnd.api+json' )
171
+ res = @app.new.handle( req )
172
+
173
+ expect( res.status ).to eq( 200 )
174
+ expect( res.content_type ).to eq( 'application/vnd.api+json' )
175
+ expect( res ).to have_json_body( Object ).
176
+ that_includes(
177
+ data: {
178
+ attributes: {
179
+ name: user.name,
180
+ email: user.email,
181
+ login: user.login,
182
+ },
183
+ id: user.id.to_s,
184
+ links: {
185
+ self: "/api/v1/users/#{user.id}"
186
+ },
187
+ type: "users"
188
+ }
189
+ )
190
+ end
191
+
192
+
193
+ it "can render a collection JSON-API response" do
194
+ users = user_generator.take( 10 )
195
+
196
+ req = @request_factory.get( "/api/v1/users", accept: 'application/vnd.api+json' )
197
+ res = @app.new.handle( req )
198
+
199
+ expect( res.status ).to eq( 200 )
200
+ expect( res.content_type ).to eq( 'application/vnd.api+json' )
201
+ expect( res ).to have_json_body( Object ).
202
+ that_includes(
203
+ data: a_collection_containing_exactly(
204
+ *users.map {|u| a_hash_including(id: u.id.to_s) }
205
+ )
206
+ )
207
+ end
208
+
209
+
210
+ it "can render a singleton Collection+JSON response" do
211
+ user = user_factory.create
212
+
213
+ req = @request_factory.get(
214
+ "/api/v1/users/#{user.id}",
215
+ accept: 'application/vnd.collection+json'
216
+ )
217
+ res = @app.new.handle( req )
218
+
219
+ expect( res.status ).to eq( 200 )
220
+ expect( res.content_type ).to eq( 'application/vnd.collection+json' )
221
+ expect( res ).to have_json_body( Object ).
222
+ that_includes(
223
+ collection: {
224
+ version: '1.0',
225
+ items: [
226
+ {
227
+ data: a_collection_containing_exactly(
228
+ { name: "name", value: user.name},
229
+ { name: "email", value: user.email},
230
+ { name: "login", value: user.login},
231
+ { name: "id", value: user.id},
232
+ ),
233
+ href: "/api/v1/users/#{user.id}"
234
+ }
235
+ ],
236
+ href: "/api/v1/users/#{user.id}"
237
+ }
238
+ )
239
+ end
240
+
241
+
242
+ it "can render a collection Collection+JSON response" do
243
+ users = user_generator.take( 10 )
244
+
245
+ req = @request_factory.get(
246
+ "/api/v1/users",
247
+ accept: 'application/vnd.collection+json'
248
+ )
249
+ res = @app.new.handle( req )
250
+
251
+ expect( res.status ).to eq( 200 )
252
+ expect( res.content_type ).to eq( 'application/vnd.collection+json' )
253
+ expect( res ).to have_json_body( Object ).
254
+ that_includes( :collection )
255
+ end
256
+
257
+ end
258
+
259
+ end
260
+
@@ -0,0 +1,22 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../../spec_helper'
5
+
6
+ require 'strelka/httpresponse/presenters'
7
+
8
+
9
+ RSpec.describe Strelka::HTTPResponse::Presenters do
10
+
11
+ it "adds an index operator to the response to allow it to quack like a Rack ENV" do
12
+ dummy_response_class = Class.new
13
+ dummy_response_class.include( described_class )
14
+ dummy_response = dummy_response_class.new
15
+
16
+ expect {
17
+ dummy_response[ 'HTTP_ACCEPT' ]
18
+ }.to_not raise_error()
19
+ end
20
+
21
+ end
22
+
@@ -0,0 +1,17 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../spec_helper'
5
+
6
+ require 'rspec'
7
+ require 'strelka/presenters'
8
+
9
+
10
+ RSpec.describe( Strelka::Presenters ) do
11
+
12
+ it "knows what version of the library it is" do
13
+ expect( described_class::VERSION ).to be_a( String ).and( match(/\A\d+\.\d+\.\d+\z/) )
14
+ end
15
+
16
+ end
17
+
@@ -0,0 +1,20 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ module Testlib
5
+
6
+ autoload :Base, 'testlib/base'
7
+ autoload :Group, 'testlib/group'
8
+ autoload :User, 'testlib/user'
9
+ autoload :Fixtures, 'testlib/fixtures'
10
+ autoload :Presenters, 'testlib/presenters'
11
+
12
+
13
+ def self::reset
14
+ Testlib::Base::DATASTORE.clear
15
+ Testlib::Base::SERIAL_GENERATOR.clear
16
+ end
17
+
18
+ end
19
+
20
+
@@ -0,0 +1,57 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require 'testlib' unless defined?( Testlib )
5
+
6
+
7
+ class Testlib::Base
8
+
9
+ # Fancy database simulator
10
+ DATASTORE = Hash.new do |h, modelclass|
11
+ h[ modelclass ] = {}
12
+ end
13
+ SERIAL_GENERATOR = Hash.new do |h, modelclass|
14
+ h[ modelclass ] = Enumerator.new do |yielder|
15
+ i = 0
16
+ loop { i += 1; yielder.yield(i) }
17
+ end
18
+ end
19
+
20
+
21
+ def self::[]( id )
22
+ return DATASTORE[ self ][ id ]
23
+ end
24
+
25
+ def self::all
26
+ return DATASTORE[ self ].values
27
+ end
28
+
29
+
30
+ def initialize( params={} )
31
+ params.each do |name, value|
32
+ self.send( "#{name}=", value )
33
+ end
34
+ end
35
+
36
+
37
+ attr_accessor :id
38
+ alias_method :pk, :id
39
+
40
+
41
+ def []( attr_name )
42
+ return self.public_send( param )
43
+ end
44
+
45
+
46
+ def save
47
+ self.id ||= SERIAL_GENERATOR[ self.class ].next
48
+ DATASTORE[ self.class ][ self.id ] = self.dup
49
+ end
50
+
51
+
52
+ def saved?
53
+ return self.id && DATASTORE[ self.class ].key?( self.id )
54
+ end
55
+
56
+ end
57
+
@@ -0,0 +1,22 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require 'fluent_fixtures'
5
+ require 'testlib' unless defined?( Testlib )
6
+
7
+ module Testlib::Fixtures
8
+ extend FluentFixtures::Collection
9
+
10
+ fixture_path_prefix 'testlib/fixtures'
11
+
12
+
13
+ def self::describe
14
+ desc = "%p: %d fixtures loaded" % [ self, self.modules.length ]
15
+ unless self.modules.empty?
16
+ desc << ": %s" % [ self.modules.keys.map(&:to_s).join(', ') ]
17
+ end
18
+ return desc
19
+ end
20
+
21
+ end
22
+
@@ -0,0 +1,12 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require 'faker'
5
+ require 'testlib/fixtures'
6
+ require 'testlib/group'
7
+
8
+ module Testlib::Fixtures::Groups
9
+ extend Testlib::Fixtures
10
+ fixtured_class Testlib::Group
11
+ end
12
+
@@ -0,0 +1,28 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require 'faker'
5
+ require 'testlib/fixtures'
6
+ require 'testlib/user'
7
+
8
+ module Testlib::Fixtures::Users
9
+ extend Testlib::Fixtures
10
+ fixtured_class Testlib::User
11
+
12
+ decorator :with_random_first_name do
13
+ self.first_name = Faker::Name.first_name
14
+ end
15
+
16
+ decorator :with_random_last_name do
17
+ self.last_name = Faker::Name.last_name
18
+ end
19
+
20
+ compose :with_random_name => [ :with_random_first_name, :with_random_last_name ]
21
+
22
+ compose :full => :with_random_name do
23
+ self.login = "%s%s" % [ self.first_name[0,1].downcase, self.last_name.downcase ]
24
+ self.email = "%s@example.com" % [ self.first_name.downcase ]
25
+ end
26
+
27
+ end
28
+
@@ -0,0 +1,19 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require 'set'
5
+ require 'testlib' unless defined?( Testlib )
6
+ require 'testlib/base'
7
+
8
+
9
+ class Testlib::Group < Testlib::Base
10
+
11
+ def initialize( * )
12
+ super
13
+ @members ||= Set.new
14
+ end
15
+
16
+ attr_accessor :name, :members
17
+
18
+ end
19
+
@@ -0,0 +1,20 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require 'yaks'
5
+
6
+ require 'testlib' unless defined?( Testlib )
7
+
8
+
9
+ # Presentation entities
10
+ module Testlib::Presenters
11
+
12
+ class UserMapper < Yaks::Mapper
13
+ link :self, '/api/v1/users/{id}'
14
+
15
+ attributes :id, :name, :email, :login
16
+
17
+ end # class PersonaMapper
18
+
19
+ end # module Prestigio::Service::Presenters
20
+
@@ -0,0 +1,24 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require 'set'
5
+ require 'testlib' unless defined?( Testlib )
6
+ require 'testlib/base'
7
+
8
+
9
+ class Testlib::User < Testlib::Base
10
+
11
+ def initialize( * )
12
+ super
13
+ @roles ||= Set.new
14
+ end
15
+
16
+ attr_accessor :first_name, :last_name, :email, :login, :groups
17
+
18
+
19
+ def name
20
+ return [ self.first_name, self.last_name ].compact.join( ' ' )
21
+ end
22
+
23
+ end
24
+
metadata ADDED
@@ -0,0 +1,262 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: strelka-presenters
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Michael Granger
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain:
11
+ - |
12
+ -----BEGIN CERTIFICATE-----
13
+ MIIENDCCApygAwIBAgIBATANBgkqhkiG9w0BAQsFADAiMSAwHgYDVQQDDBdnZWQv
14
+ REM9RmFlcmllTVVEL0RDPW9yZzAeFw0xODExMjAxODI5NTlaFw0xOTExMjAxODI5
15
+ NTlaMCIxIDAeBgNVBAMMF2dlZC9EQz1GYWVyaWVNVUQvREM9b3JnMIIBojANBgkq
16
+ hkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAvyVhkRzvlEs0fe7145BYLfN6njX9ih5H
17
+ L60U0p0euIurpv84op9CNKF9tx+1WKwyQvQP7qFGuZxkSUuWcP/sFhDXL1lWUuIl
18
+ M4uHbGCRmOshDrF4dgnBeOvkHr1fIhPlJm5FO+Vew8tSQmlDsosxLUx+VB7DrVFO
19
+ 5PU2AEbf04GGSrmqADGWXeaslaoRdb1fu/0M5qfPTRn5V39sWD9umuDAF9qqil/x
20
+ Sl6phTvgBrG8GExHbNZpLARd3xrBYLEFsX7RvBn2UPfgsrtvpdXjsHGfpT3IPN+B
21
+ vQ66lts4alKC69TE5cuKasWBm+16A4aEe3XdZBRNmtOu/g81gvwA7fkJHKllJuaI
22
+ dXzdHqq+zbGZVSQ7pRYHYomD0IiDe1DbIouFnPWmagaBnGHwXkDT2bKKP+s2v21m
23
+ ozilJg4aar2okb/RA6VS87o+d7g6LpDDMMQjH4G9OPnJENLdhu8KnPw/ivSVvQw7
24
+ N2I4L/ZOIe2DIVuYH7aLHfjZDQv/mNgpAgMBAAGjdTBzMAkGA1UdEwQCMAAwCwYD
25
+ VR0PBAQDAgSwMB0GA1UdDgQWBBRyjf55EbrHagiRLqt5YAd3yb8k4DAcBgNVHREE
26
+ FTATgRFnZWRARmFlcmllTVVELm9yZzAcBgNVHRIEFTATgRFnZWRARmFlcmllTVVE
27
+ Lm9yZzANBgkqhkiG9w0BAQsFAAOCAYEAP9Ffkvg4e8CjIWi8SykQ8oJSS8jbmbgF
28
+ abke3vXWLG6V9kFiObuJd5wZRBluJANu7bEtjgc3fFaGVP2XxVdCpVjNbmMDg4Qp
29
+ ovvczP53X6pQP2RSZgxF6Lblvy8y11RziUTVRG/Z2aJHsElo6gI7vQznE/OSDrhC
30
+ gEhr8uaIUt7D+HZWRbU0+MkKPpL5uMqaFuJbqXEvSwPTuUuYkDfNfsjQO7ruWBac
31
+ bxHCrvpZ6Tijc0nrlyXi6gPOCLeaqhau2xFnlvKgELwsGYSoKBJyDwqtQ5kwrOlU
32
+ tkSyLrfZ+RZcH535Hyvif7ZxB0v5OxXXoec+N2vrUsEUMRDL9dg4/WFdN8hIOixF
33
+ 3IPKpZ1ho0Ya5q7yhygtBK9/NBFHw+nbJjcltfPDBXleRe8u73gnQo8AZIhStYSP
34
+ v4qqqa27Bs468d6SoPxjSm8a2mM9HZ4OdWhq4tFsbTeXDVquCfi64OTEaTt2xQdR
35
+ JnC4lpJfCP6aCXa5h2XAQfPSH636cQap
36
+ -----END CERTIFICATE-----
37
+ date: 2019-08-25 00:00:00.000000000 Z
38
+ dependencies:
39
+ - !ruby/object:Gem::Dependency
40
+ name: strelka
41
+ requirement: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - "~>"
44
+ - !ruby/object:Gem::Version
45
+ version: '0.16'
46
+ type: :runtime
47
+ prerelease: false
48
+ version_requirements: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - "~>"
51
+ - !ruby/object:Gem::Version
52
+ version: '0.16'
53
+ - !ruby/object:Gem::Dependency
54
+ name: yaks
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '0.13'
60
+ type: :runtime
61
+ prerelease: false
62
+ version_requirements: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: '0.13'
67
+ - !ruby/object:Gem::Dependency
68
+ name: faker
69
+ requirement: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: '2.1'
74
+ type: :runtime
75
+ prerelease: false
76
+ version_requirements: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - "~>"
79
+ - !ruby/object:Gem::Version
80
+ version: '2.1'
81
+ - !ruby/object:Gem::Dependency
82
+ name: fluent_fixtures
83
+ requirement: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - "~>"
86
+ - !ruby/object:Gem::Version
87
+ version: '0.8'
88
+ type: :runtime
89
+ prerelease: false
90
+ version_requirements: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - "~>"
93
+ - !ruby/object:Gem::Version
94
+ version: '0.8'
95
+ - !ruby/object:Gem::Dependency
96
+ name: hoe-mercurial
97
+ requirement: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - "~>"
100
+ - !ruby/object:Gem::Version
101
+ version: '1.4'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - "~>"
107
+ - !ruby/object:Gem::Version
108
+ version: '1.4'
109
+ - !ruby/object:Gem::Dependency
110
+ name: hoe-deveiate
111
+ requirement: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - "~>"
114
+ - !ruby/object:Gem::Version
115
+ version: '0.10'
116
+ type: :development
117
+ prerelease: false
118
+ version_requirements: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - "~>"
121
+ - !ruby/object:Gem::Version
122
+ version: '0.10'
123
+ - !ruby/object:Gem::Dependency
124
+ name: hoe-highline
125
+ requirement: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - "~>"
128
+ - !ruby/object:Gem::Version
129
+ version: '0.2'
130
+ type: :development
131
+ prerelease: false
132
+ version_requirements: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - "~>"
135
+ - !ruby/object:Gem::Version
136
+ version: '0.2'
137
+ - !ruby/object:Gem::Dependency
138
+ name: simplecov
139
+ requirement: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - "~>"
142
+ - !ruby/object:Gem::Version
143
+ version: '0.17'
144
+ type: :development
145
+ prerelease: false
146
+ version_requirements: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - "~>"
149
+ - !ruby/object:Gem::Version
150
+ version: '0.17'
151
+ - !ruby/object:Gem::Dependency
152
+ name: rdoc-generator-fivefish
153
+ requirement: !ruby/object:Gem::Requirement
154
+ requirements:
155
+ - - "~>"
156
+ - !ruby/object:Gem::Version
157
+ version: '0.1'
158
+ type: :development
159
+ prerelease: false
160
+ version_requirements: !ruby/object:Gem::Requirement
161
+ requirements:
162
+ - - "~>"
163
+ - !ruby/object:Gem::Version
164
+ version: '0.1'
165
+ - !ruby/object:Gem::Dependency
166
+ name: rdoc
167
+ requirement: !ruby/object:Gem::Requirement
168
+ requirements:
169
+ - - ">="
170
+ - !ruby/object:Gem::Version
171
+ version: '4.0'
172
+ - - "<"
173
+ - !ruby/object:Gem::Version
174
+ version: '7'
175
+ type: :development
176
+ prerelease: false
177
+ version_requirements: !ruby/object:Gem::Requirement
178
+ requirements:
179
+ - - ">="
180
+ - !ruby/object:Gem::Version
181
+ version: '4.0'
182
+ - - "<"
183
+ - !ruby/object:Gem::Version
184
+ version: '7'
185
+ - !ruby/object:Gem::Dependency
186
+ name: hoe
187
+ requirement: !ruby/object:Gem::Requirement
188
+ requirements:
189
+ - - "~>"
190
+ - !ruby/object:Gem::Version
191
+ version: '3.18'
192
+ type: :development
193
+ prerelease: false
194
+ version_requirements: !ruby/object:Gem::Requirement
195
+ requirements:
196
+ - - "~>"
197
+ - !ruby/object:Gem::Version
198
+ version: '3.18'
199
+ description: |-
200
+ Strelka-Presenters is a plugin for the Strelka web framework that adds
201
+ integration with the [Yaks hypermedia library][Yaks].
202
+ email:
203
+ - ged@FaerieMUD.org
204
+ executables: []
205
+ extensions: []
206
+ extra_rdoc_files:
207
+ - History.md
208
+ - LICENSE.txt
209
+ - Manifest.txt
210
+ - README.md
211
+ files:
212
+ - ".document"
213
+ - ".rdoc_options"
214
+ - ".simplecov"
215
+ - ChangeLog
216
+ - History.md
217
+ - LICENSE.txt
218
+ - Manifest.txt
219
+ - README.md
220
+ - Rakefile
221
+ - lib/strelka/app/presenters.rb
222
+ - lib/strelka/httpresponse/presenters.rb
223
+ - lib/strelka/presenters.rb
224
+ - spec/spec_helper.rb
225
+ - spec/strelka/app/presenters_spec.rb
226
+ - spec/strelka/httpresponse/presenters_spec.rb
227
+ - spec/strelka/presenters_spec.rb
228
+ - spec/testlib.rb
229
+ - spec/testlib/base.rb
230
+ - spec/testlib/fixtures.rb
231
+ - spec/testlib/fixtures/groups.rb
232
+ - spec/testlib/fixtures/users.rb
233
+ - spec/testlib/group.rb
234
+ - spec/testlib/presenters.rb
235
+ - spec/testlib/user.rb
236
+ homepage: https://bitbucket.org/ged/strelka-presenters
237
+ licenses:
238
+ - BSD-3-Clause
239
+ metadata: {}
240
+ post_install_message:
241
+ rdoc_options:
242
+ - "--main"
243
+ - README.md
244
+ require_paths:
245
+ - lib
246
+ required_ruby_version: !ruby/object:Gem::Requirement
247
+ requirements:
248
+ - - ">="
249
+ - !ruby/object:Gem::Version
250
+ version: 2.5.0
251
+ required_rubygems_version: !ruby/object:Gem::Requirement
252
+ requirements:
253
+ - - ">="
254
+ - !ruby/object:Gem::Version
255
+ version: '0'
256
+ requirements: []
257
+ rubygems_version: 3.0.3
258
+ signing_key:
259
+ specification_version: 4
260
+ summary: Strelka-Presenters is a plugin for the Strelka web framework that adds integration
261
+ with the [Yaks hypermedia library][Yaks].
262
+ test_files: []
Binary file