podium 0.4.0 → 0.5.0
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/.gitignore +6 -6
- data/.ruby-version +1 -1
- data/Gemfile +9 -6
- data/MIT-LICENSE +20 -0
- data/Rakefile +8 -4
- data/app/controllers/podium/podlet_preview_controller.rb +26 -0
- data/bin/test +5 -0
- data/lib/generators/podium/install_generator.rb +43 -0
- data/lib/generators/podium/podlet_generator.rb +90 -0
- data/lib/generators/podium/templates/content.html.erb.tt +1 -0
- data/lib/generators/podium/templates/podium.rb.tt +8 -0
- data/lib/generators/podium/templates/podlet_controller.rb.tt +10 -0
- data/lib/generators/podium/templates/podlet_preview_controller.rb.tt +3 -0
- data/lib/podium/configuration.rb +19 -0
- data/lib/podium/controller_helpers.rb +20 -0
- data/lib/podium/core_ext.rb +25 -0
- data/lib/podium/podlet_helpers.rb +59 -0
- data/lib/podium/podlet_preview.rb +11 -0
- data/lib/podium/railtie.rb +4 -0
- data/lib/podium/version.rb +1 -1
- data/lib/podium.rb +26 -2
- data/lib/tasks/podium_tasks.rake +4 -0
- data/podium.gemspec +1 -5
- metadata +33 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3e08896c276aabb94c3b3440cdcf6a01d08b202eb3f0a5842d8ac4a05dfd28ec
|
4
|
+
data.tar.gz: 2b1b7aa523cf047954bef1a04be016d32895c5f9bc43f8c86114e19d33ab81aa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2c12272a1862cb7043c7e99a6260c27148548b3630c6a204dd7336cdd03bcd85af472418a009b9fef86f6c5865e148cc7acd3d240e97a4aa72400517beaf2ee2
|
7
|
+
data.tar.gz: 73d86063551decae0fa53b1d75710d24478eeeb8a8fb0690144156b2382f65c1a2db4f4cd3b0be2bbc87a4b62afb82e8976b85fca6b56d72906b12f621b00f0f
|
data/.gitignore
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
/.bundle/
|
2
|
-
/.yardoc
|
3
|
-
/_yardoc/
|
4
|
-
/coverage/
|
5
2
|
/doc/
|
3
|
+
/log/*.log
|
6
4
|
/pkg/
|
7
|
-
/spec/reports/
|
8
5
|
/tmp/
|
6
|
+
/test/dummy/db/*.sqlite3
|
7
|
+
/test/dummy/db/*.sqlite3-*
|
8
|
+
/test/dummy/log/*.log
|
9
|
+
/test/dummy/storage/
|
10
|
+
/test/dummy/tmp/
|
9
11
|
|
10
|
-
# rspec failure tracking
|
11
|
-
.rspec_status
|
12
12
|
Gemfile.lock
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
3.0.
|
1
|
+
3.0.2
|
data/Gemfile
CHANGED
@@ -1,12 +1,15 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
source "https://rubygems.org"
|
2
|
+
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
|
4
3
|
|
5
|
-
# Specify your gem's dependencies in podium.gemspec
|
4
|
+
# Specify your gem's dependencies in podium.gemspec.
|
6
5
|
gemspec
|
7
6
|
|
8
|
-
|
7
|
+
group :development do
|
8
|
+
gem "sqlite3"
|
9
|
+
end
|
9
10
|
|
10
|
-
|
11
|
+
# Start debugger with binding.b -- Read more: https://github.com/ruby/debug
|
12
|
+
gem "debug", ">= 1.0.0", group: %i[ development test ]
|
11
13
|
|
12
|
-
gem "
|
14
|
+
gem "rake"
|
15
|
+
gem "webmock"
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2021 Max Brosnahan
|
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.
|
data/Rakefile
CHANGED
@@ -1,8 +1,12 @@
|
|
1
|
-
|
1
|
+
require "bundler/setup"
|
2
2
|
|
3
3
|
require "bundler/gem_tasks"
|
4
|
-
require "
|
4
|
+
require "rake/testtask"
|
5
5
|
|
6
|
-
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
7
|
+
t.libs << "test"
|
8
|
+
t.pattern = "test/**/*_test.rb"
|
9
|
+
t.verbose = false
|
10
|
+
end
|
7
11
|
|
8
|
-
task default: :
|
12
|
+
task default: :test
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Podium
|
2
|
+
class PodletPreviewController < ActionController::Base
|
3
|
+
def index
|
4
|
+
@podlets = Podium::PodletPreview.podlets
|
5
|
+
links = @podlets.map do |podlet|
|
6
|
+
name = podlet.to_s.underscore
|
7
|
+
%(<li><a href="#{podlet_preview_path}/#{name.dasherize}"> #{name.humanize}</a></li>)
|
8
|
+
end.join("\n")
|
9
|
+
render html: <<-HTML.html_safe
|
10
|
+
<h1>Podlet Preview</h1>
|
11
|
+
<ul>
|
12
|
+
#{links}
|
13
|
+
</ul>
|
14
|
+
HTML
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def podlet_resource(name, uri: nil)
|
20
|
+
podium_name = name.to_s.dasherize
|
21
|
+
uri ||= URI(request.url).tap { |u| u.path = "/#{podium_name}" }
|
22
|
+
|
23
|
+
Podium::Resource.new(uri, podium_name)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/bin/test
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require "rails/generators"
|
2
|
+
|
3
|
+
module Podium
|
4
|
+
class InstallGenerator < ::Rails::Generators::Base
|
5
|
+
APP_CONTROLLER_PATH = "app/controllers/application_controller.rb"
|
6
|
+
LAYOUT_PATH = "app/views/layouts/application.html.erb"
|
7
|
+
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
9
|
+
|
10
|
+
desc "Sets up your application for integrating with podium"
|
11
|
+
|
12
|
+
def copy_initializer
|
13
|
+
template "podium.rb", "config/initializers/podium.rb"
|
14
|
+
|
15
|
+
inside do
|
16
|
+
setup_app_controller
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def add_podlets_to_layout
|
21
|
+
return if File.read(LAYOUT_PATH).include?("podlet_content!")
|
22
|
+
inject_into_file LAYOUT_PATH, "<%= podlet_content!(:header) %>\n", before: "<%= yield %>"
|
23
|
+
inject_into_file LAYOUT_PATH, "<%= podlet_content!(:footer) %>\n", after: "<%= yield %>\n"
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def setup_app_controller
|
29
|
+
return unless File.exists?(APP_CONTROLLER_PATH)
|
30
|
+
|
31
|
+
return if File.read(APP_CONTROLLER_PATH).include?("Podium::ControllerHelpers")
|
32
|
+
|
33
|
+
new_content = <<-'RUBY'
|
34
|
+
include Podium::ControllerHelpers
|
35
|
+
|
36
|
+
def podlets
|
37
|
+
[:header, :footer]
|
38
|
+
end
|
39
|
+
RUBY
|
40
|
+
inject_into_class APP_CONTROLLER_PATH, "ApplicationController", new_content
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require "rails/generators"
|
2
|
+
|
3
|
+
module Podium
|
4
|
+
class PodletGenerator < ::Rails::Generators::NamedBase
|
5
|
+
APP_CONTROLLER_PATH = "app/controllers/application_controller.rb"
|
6
|
+
PREVIEW_CONTROLLER_PATH = "app/controllers/podlet_preview_controller.rb"
|
7
|
+
LAYOUT_PATH = "app/views/layouts/application.html.erb"
|
8
|
+
|
9
|
+
source_root File.expand_path("templates", __dir__)
|
10
|
+
|
11
|
+
desc "Generate boilerplate for a podlet"
|
12
|
+
|
13
|
+
def create_controller
|
14
|
+
template "podlet_controller.rb", "app/controllers/#{controller_file_name}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def create_views
|
18
|
+
template "content.html.erb", "app/views/#{folder_name}/content.html.erb"
|
19
|
+
end
|
20
|
+
|
21
|
+
def prepare_assets
|
22
|
+
create_file "app/assets/javascripts/#{folder_name}/podlet.js"
|
23
|
+
create_file "app/assets/stylesheets/#{folder_name}/podlet.css"
|
24
|
+
return if contents("app/assets/config/manifest.js").include?(folder_name)
|
25
|
+
append_to_file "app/assets/config/manifest.js", "//= link_directory ../javascripts/#{folder_name} .js\n"
|
26
|
+
append_to_file "app/assets/config/manifest.js", "//= link_directory ../stylesheets/#{folder_name} .css\n"
|
27
|
+
end
|
28
|
+
|
29
|
+
def setup_routes
|
30
|
+
route = "podlet :#{podlet_name}"
|
31
|
+
return if route_present?(route)
|
32
|
+
if route_present?("podlet_preview")
|
33
|
+
inject_into_file "config/routes.rb", "\n #{route}", before: "\n podlet_preview"
|
34
|
+
else
|
35
|
+
inject_into_file "config/routes.rb", "\n #{route}", before: "\nend"
|
36
|
+
inject_into_file "config/routes.rb", "\n podlet_preview if Rails.env.development?", before: "\nend"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def setup_preview
|
41
|
+
unless preview_exists?
|
42
|
+
template "podlet_preview_controller.rb", PREVIEW_CONTROLLER_PATH
|
43
|
+
end
|
44
|
+
return if contents(PREVIEW_CONTROLLER_PATH).include?(podlet_name)
|
45
|
+
|
46
|
+
podlet_action = <<-RUBY
|
47
|
+
\ndef #{podlet_name}
|
48
|
+
resource = podlet_resource(:#{podlet_name})
|
49
|
+
render html: resource.fetch({}).html_safe
|
50
|
+
end
|
51
|
+
RUBY
|
52
|
+
|
53
|
+
inject_into_file PREVIEW_CONTROLLER_PATH, podlet_action, before: "\nend"
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def preview_exists?
|
59
|
+
inside do
|
60
|
+
return File.exist?(PREVIEW_CONTROLLER_PATH)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def route_present?(route)
|
65
|
+
contents("config/routes.rb").include?(route)
|
66
|
+
end
|
67
|
+
|
68
|
+
def contents(path)
|
69
|
+
inside do
|
70
|
+
return File.read(path)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def folder_name
|
75
|
+
"#{podlet_name}_podlet"
|
76
|
+
end
|
77
|
+
|
78
|
+
def podlet_name
|
79
|
+
name.underscore
|
80
|
+
end
|
81
|
+
|
82
|
+
def controller_file_name
|
83
|
+
"#{controller_name.underscore}.rb"
|
84
|
+
end
|
85
|
+
|
86
|
+
def controller_name
|
87
|
+
"#{name.underscore.camelcase}PodletController"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
<strong>hello from podlet</strong>
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Podium
|
2
|
+
class Configuration
|
3
|
+
attr_reader :podlets
|
4
|
+
|
5
|
+
attr_writer :name_to_url
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@podlets = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def register(name, url = nil)
|
12
|
+
@podlets[name] = url
|
13
|
+
end
|
14
|
+
|
15
|
+
def name_to_url
|
16
|
+
@name_to_url || ->(name) { raise "name_to_url not configured. Set name_to_url in podium configuration." }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Podium
|
2
|
+
module ControllerHelpers
|
3
|
+
def self.included(base)
|
4
|
+
base.before_action :load_podlet_content
|
5
|
+
base.helper_method :podlet_content!
|
6
|
+
end
|
7
|
+
|
8
|
+
def podlet_content!(name)
|
9
|
+
@podlet_content.fetch(name).html_safe
|
10
|
+
end
|
11
|
+
|
12
|
+
def load_podlet_content
|
13
|
+
@podlet_content = Podium.instance.load_content_for_podlets(podlets, podlet_context)
|
14
|
+
end
|
15
|
+
|
16
|
+
def podlet_context
|
17
|
+
{}
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module ActionDispatch
|
2
|
+
module Routing
|
3
|
+
class Mapper
|
4
|
+
def podlet(name)
|
5
|
+
Podium::PodletPreview.register(name)
|
6
|
+
prefix = "#{name}_podlet"
|
7
|
+
scope name.to_s.dasherize, as: prefix do
|
8
|
+
get "/", to: "#{prefix}#content", as: "content"
|
9
|
+
get "/fallback", to: "#{prefix}#fallback"
|
10
|
+
get "/manifest.json", to: "#{prefix}#manifest", as: "manifest"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def podlet_preview
|
15
|
+
scope "podlet-preview" do
|
16
|
+
get "/", to: "podlet_preview#index", as: "podlet_preview"
|
17
|
+
|
18
|
+
Podium::PodletPreview.podlets.each do |name|
|
19
|
+
get "/#{name.to_s.dasherize}", to: "podlet_preview##{name}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Podium
|
2
|
+
module PodletHelpers
|
3
|
+
def self.included(base)
|
4
|
+
base.layout false
|
5
|
+
base.extend(ClassMethods)
|
6
|
+
base.before_action :set_podium_headers, only: [:content, :fallback]
|
7
|
+
end
|
8
|
+
|
9
|
+
def manifest
|
10
|
+
render json: self.class.podlet_manifest.to_json
|
11
|
+
end
|
12
|
+
|
13
|
+
def set_podium_headers
|
14
|
+
response.set_header("podlet-version", self.class.version)
|
15
|
+
end
|
16
|
+
|
17
|
+
def podium_params
|
18
|
+
@podium_params ||= ActionController::Parameters.new(podium_header_params)
|
19
|
+
end
|
20
|
+
|
21
|
+
def podium_header_params
|
22
|
+
request.headers
|
23
|
+
.select { |(k)| k.start_with?("HTTP_PODIUM_") }
|
24
|
+
.reduce({}) do |acc, (header, value)|
|
25
|
+
key = header.gsub("HTTP_PODIUM_", "").underscore
|
26
|
+
acc[key] = value
|
27
|
+
acc
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
module ClassMethods
|
32
|
+
def podlet_manifest
|
33
|
+
@podlet_manifest ||= {
|
34
|
+
name: self.class.name.gsub("PodletController", "").underscore.dasherize,
|
35
|
+
version: version,
|
36
|
+
content: "/",
|
37
|
+
fallback: "/fallback",
|
38
|
+
proxy: {},
|
39
|
+
|
40
|
+
}.merge(assets)
|
41
|
+
end
|
42
|
+
|
43
|
+
def assets
|
44
|
+
{
|
45
|
+
assets: {
|
46
|
+
js: "",
|
47
|
+
css: "",
|
48
|
+
},
|
49
|
+
js: [],
|
50
|
+
css: [],
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
def version
|
55
|
+
@version ||= ENV.fetch("VERSION", DateTime.now().to_i.to_s)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/podium/version.rb
CHANGED
data/lib/podium.rb
CHANGED
@@ -4,9 +4,33 @@ require_relative "podium/version"
|
|
4
4
|
require_relative "podium/client"
|
5
5
|
require_relative "podium/resource"
|
6
6
|
require_relative "podium/manifest"
|
7
|
+
require_relative "podium/configuration"
|
8
|
+
require_relative "podium/controller_helpers"
|
9
|
+
require_relative "podium/podlet_preview"
|
10
|
+
require_relative "podium/podlet_helpers"
|
11
|
+
require_relative "podium/core_ext"
|
7
12
|
|
8
13
|
module Podium
|
9
|
-
class
|
14
|
+
class NotConfiguredError < StandardError; end
|
10
15
|
|
11
|
-
|
16
|
+
def self.configuration
|
17
|
+
@configuration || (raise NotConfiguredError, "Did you forget to run Podium.configure ?")
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.configure
|
21
|
+
@configuration ||= Configuration.new
|
22
|
+
yield(configuration)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.instance
|
26
|
+
@instance ||= build_instance
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.build_instance
|
30
|
+
client = Podium::Client.new(name_to_url: configuration.name_to_url)
|
31
|
+
configuration.podlets.each do |key, url|
|
32
|
+
client.register(key, url)
|
33
|
+
end
|
34
|
+
client
|
35
|
+
end
|
12
36
|
end
|
data/podium.gemspec
CHANGED
@@ -28,9 +28,5 @@ Gem::Specification.new do |spec|
|
|
28
28
|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
29
29
|
spec.require_paths = ["lib"]
|
30
30
|
|
31
|
-
|
32
|
-
# spec.add_dependency "example-gem", "~> 1.0"
|
33
|
-
|
34
|
-
# For more information and examples about making a new gem, checkout our
|
35
|
-
# guide at: https://bundler.io/guides/creating_gem.html
|
31
|
+
spec.add_dependency "rails", "> 6.1"
|
36
32
|
end
|
metadata
CHANGED
@@ -1,15 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: podium
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Max Brosnahan
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
12
|
-
dependencies:
|
11
|
+
date: 2021-10-02 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '6.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '6.1'
|
13
27
|
description:
|
14
28
|
email:
|
15
29
|
- maximilianbrosnahan@gmail.com
|
@@ -25,15 +39,31 @@ files:
|
|
25
39
|
- CODE_OF_CONDUCT.md
|
26
40
|
- Gemfile
|
27
41
|
- LICENSE.txt
|
42
|
+
- MIT-LICENSE
|
28
43
|
- README.md
|
29
44
|
- Rakefile
|
45
|
+
- app/controllers/podium/podlet_preview_controller.rb
|
30
46
|
- bin/console
|
31
47
|
- bin/setup
|
48
|
+
- bin/test
|
49
|
+
- lib/generators/podium/install_generator.rb
|
50
|
+
- lib/generators/podium/podlet_generator.rb
|
51
|
+
- lib/generators/podium/templates/content.html.erb.tt
|
52
|
+
- lib/generators/podium/templates/podium.rb.tt
|
53
|
+
- lib/generators/podium/templates/podlet_controller.rb.tt
|
54
|
+
- lib/generators/podium/templates/podlet_preview_controller.rb.tt
|
32
55
|
- lib/podium.rb
|
33
56
|
- lib/podium/client.rb
|
57
|
+
- lib/podium/configuration.rb
|
58
|
+
- lib/podium/controller_helpers.rb
|
59
|
+
- lib/podium/core_ext.rb
|
34
60
|
- lib/podium/manifest.rb
|
61
|
+
- lib/podium/podlet_helpers.rb
|
62
|
+
- lib/podium/podlet_preview.rb
|
63
|
+
- lib/podium/railtie.rb
|
35
64
|
- lib/podium/resource.rb
|
36
65
|
- lib/podium/version.rb
|
66
|
+
- lib/tasks/podium_tasks.rake
|
37
67
|
- podium.gemspec
|
38
68
|
homepage: https://github.com/gingermusketeer/podium
|
39
69
|
licenses:
|