meetup-generator 0.0.20210128
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +84 -0
- data/LICENSE.txt +23 -0
- data/README.md +64 -0
- data/Rakefile +19 -0
- data/bin/locate_meetup-generator +6 -0
- data/bin/meetup-generator.rb +34 -0
- data/config.ru +4 -0
- data/lib/all_the_things.yaml +1127 -0
- data/lib/locator.rb +7 -0
- data/lib/meetup_generator.rb +106 -0
- data/lib/words.gz +0 -0
- data/meetup-generator.gemspec +43 -0
- data/package/meetup-generator.xml +100 -0
- data/public/css/main.css +64 -0
- data/public/favicon.ico +0 -0
- data/public/zxspectr.woff +0 -0
- data/spec/acceptance_spec.rb +116 -0
- data/spec/meetup_generator_spec.rb +138 -0
- data/views/default.slim +48 -0
- data/views/partial_talk.slim +11 -0
- metadata +236 -0
data/lib/locator.rb
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
require 'date'
|
5
|
+
require 'zlib'
|
6
|
+
|
7
|
+
LIB = Pathname.new(__dir__)
|
8
|
+
|
9
|
+
# Everything needed for a meetup generator
|
10
|
+
#
|
11
|
+
class MeetupGenerator
|
12
|
+
attr_reader :words, :lib
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@words = Zlib::GzipReader.open(LIB + 'words.gz').readlines.map(&:strip)
|
16
|
+
@lib = YAML.safe_load(IO.read(LIB + 'all_the_things.yaml'),
|
17
|
+
symbolize_names: true)
|
18
|
+
end
|
19
|
+
|
20
|
+
# @param num [Integer] how many talks you want
|
21
|
+
# @return [Hash] full meetup agenda
|
22
|
+
#
|
23
|
+
def agenda(num = 5)
|
24
|
+
{ talks: lib[:template].sample(num).map { |t| talk(t) },
|
25
|
+
refreshment: refreshment,
|
26
|
+
location: location,
|
27
|
+
date: date }
|
28
|
+
end
|
29
|
+
|
30
|
+
# @param templates [Array[String]] array of templates
|
31
|
+
#
|
32
|
+
def talk(template = random_template)
|
33
|
+
{ title: title(template),
|
34
|
+
talker: talker,
|
35
|
+
role: role,
|
36
|
+
company: company }
|
37
|
+
end
|
38
|
+
|
39
|
+
def location
|
40
|
+
'Shoreditch, probably'
|
41
|
+
end
|
42
|
+
|
43
|
+
def date
|
44
|
+
(Date.today + 1).strftime('%d/%m/%Y')
|
45
|
+
end
|
46
|
+
|
47
|
+
def title(template = random_template)
|
48
|
+
replace_word(replace_number(replace_ops(replace_things(template))))
|
49
|
+
end
|
50
|
+
|
51
|
+
def random_template(number = 1)
|
52
|
+
lib[:template].sample(number).first
|
53
|
+
end
|
54
|
+
|
55
|
+
def talker
|
56
|
+
pair(:first_name, :last_name)
|
57
|
+
end
|
58
|
+
|
59
|
+
def role
|
60
|
+
pair(:job_role, :job_title)
|
61
|
+
end
|
62
|
+
|
63
|
+
def refreshment
|
64
|
+
pair(:food_style, :food)
|
65
|
+
end
|
66
|
+
|
67
|
+
def company
|
68
|
+
format('%<word>s.io', word: words.sample.sub(/([^aeiou])er$/, '\\1r'))
|
69
|
+
end
|
70
|
+
|
71
|
+
def something_ops
|
72
|
+
(lib[:something_ops] * 4).sample(rand(2..4)).join + 'Ops'
|
73
|
+
end
|
74
|
+
|
75
|
+
def pair(list1, list2)
|
76
|
+
[lib[list1].sample, lib[list2].sample].join(' ')
|
77
|
+
end
|
78
|
+
|
79
|
+
def replace_things(template)
|
80
|
+
return template unless template =~ /%[a-z_]+%/
|
81
|
+
|
82
|
+
replace_things(template.sub(/%([a-z_]+)%/) do
|
83
|
+
lib[Regexp.last_match(1).to_sym].sample
|
84
|
+
end)
|
85
|
+
end
|
86
|
+
|
87
|
+
def replace_word(template)
|
88
|
+
return template unless template.include?('%WORD%')
|
89
|
+
|
90
|
+
replace_word(template.sub('%WORD%', words.sample.capitalize))
|
91
|
+
end
|
92
|
+
|
93
|
+
def replace_ops(template)
|
94
|
+
return template unless template.include?('%FNOPS%')
|
95
|
+
|
96
|
+
replace_ops(template.sub('%FNOPS%', something_ops))
|
97
|
+
end
|
98
|
+
|
99
|
+
def replace_number(template)
|
100
|
+
return template unless template =~ /%RAND\d+%/
|
101
|
+
|
102
|
+
replace_number(template.sub(/%RAND(\d+)%/) do
|
103
|
+
rand(2..Regexp.last_match(1).to_i).to_s
|
104
|
+
end)
|
105
|
+
end
|
106
|
+
end
|
data/lib/words.gz
ADDED
Binary file
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'date'
|
4
|
+
|
5
|
+
# Github actions sets the RELEASE_VERSION environment variable
|
6
|
+
|
7
|
+
Gem::Specification.new do |gem|
|
8
|
+
gem.name = 'meetup-generator'
|
9
|
+
gem.version = ENV['RELEASE_VERSION'] ||
|
10
|
+
"0.0.#{Time.now.strftime('%Y%m%d')}"
|
11
|
+
gem.date = Date.today.to_s
|
12
|
+
|
13
|
+
gem.summary = 'Stupid fatuous random string generatpr'
|
14
|
+
gem.description = 'Generates a website advertising a fictional ' \
|
15
|
+
'DevOps meetup, using all the latest buzzwords, ' \
|
16
|
+
'new-shiny, and clichés'
|
17
|
+
gem.authors = ['Robert Fisher']
|
18
|
+
gem.email = 'rob@sysdef.xyz'
|
19
|
+
gem.homepage = 'https://github.com/snltd/meetup-generator'
|
20
|
+
gem.license = 'BSD-2-Clause'
|
21
|
+
|
22
|
+
gem.bindir = 'bin'
|
23
|
+
gem.executables = ['meetup-generator.rb', 'locate_meetup-generator']
|
24
|
+
gem.files = IO.readlines('package/runtime_files', chomp: true)
|
25
|
+
gem.test_files = gem.files.grep(%r{^spec/})
|
26
|
+
gem.require_paths = %w[lib]
|
27
|
+
|
28
|
+
gem.add_runtime_dependency 'puma', '~> 5.1'
|
29
|
+
gem.add_runtime_dependency 'sinatra', '~>2.1'
|
30
|
+
gem.add_runtime_dependency 'slim', '~> 4.1'
|
31
|
+
|
32
|
+
gem.add_development_dependency 'bundler', '~> 2.0'
|
33
|
+
gem.add_development_dependency 'minitest', '~> 5.14'
|
34
|
+
gem.add_development_dependency 'nokogiri', '~> 1.10'
|
35
|
+
gem.add_development_dependency 'rack-test', '~> 1.1'
|
36
|
+
gem.add_development_dependency 'rake', '~> 13.0'
|
37
|
+
gem.add_development_dependency 'rubocop', '~> 1.9'
|
38
|
+
gem.add_development_dependency 'rubocop-minitest', '~> 0.10'
|
39
|
+
gem.add_development_dependency 'rubocop-performance', '~> 1.3'
|
40
|
+
gem.add_development_dependency 'rubocop-rake', '~> 0.5'
|
41
|
+
|
42
|
+
gem.required_ruby_version = Gem::Requirement.new('>= 2.5.0')
|
43
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
<?xml version='1.0'?>
|
2
|
+
|
3
|
+
<!DOCTYPE service_bundle SYSTEM '/usr/share/lib/xml/dtd/service_bundle.dtd.1'>
|
4
|
+
|
5
|
+
<service_bundle type='manifest' name='snltd:sinatra:meetup-generator'>
|
6
|
+
|
7
|
+
<service
|
8
|
+
name='sysdef/meetup-generator'
|
9
|
+
type='service'
|
10
|
+
version='1'>
|
11
|
+
|
12
|
+
<single_instance />
|
13
|
+
|
14
|
+
<property_group name='startd' type='framework'>
|
15
|
+
|
16
|
+
<stability value='Unstable' />
|
17
|
+
|
18
|
+
<propval
|
19
|
+
name='duration'
|
20
|
+
type='astring'
|
21
|
+
value='transient' />
|
22
|
+
|
23
|
+
</property_group>
|
24
|
+
|
25
|
+
<property_group name='options' type='applications'>
|
26
|
+
|
27
|
+
<propval
|
28
|
+
name='rackup'
|
29
|
+
type='astring'
|
30
|
+
value='/opt/local/bin/rackup' />
|
31
|
+
|
32
|
+
<propval
|
33
|
+
name='user'
|
34
|
+
type='astring'
|
35
|
+
value='sinatra' />
|
36
|
+
|
37
|
+
<propval
|
38
|
+
name='dir'
|
39
|
+
type='astring'
|
40
|
+
value='/www/meetup-generator' />
|
41
|
+
|
42
|
+
<propval
|
43
|
+
name='port'
|
44
|
+
type='astring'
|
45
|
+
value='8002' />
|
46
|
+
|
47
|
+
</property_group>
|
48
|
+
|
49
|
+
<instance name='default' enabled='true'>
|
50
|
+
|
51
|
+
<!-- Run in multi-user mode -->
|
52
|
+
|
53
|
+
<dependency
|
54
|
+
name='multi-user'
|
55
|
+
grouping='require_all'
|
56
|
+
restart_on='none'
|
57
|
+
type='service'>
|
58
|
+
|
59
|
+
<service_fmri value='svc:/milestone/multi-user-server:default' />
|
60
|
+
|
61
|
+
</dependency>
|
62
|
+
|
63
|
+
<exec_method
|
64
|
+
name='start'
|
65
|
+
type='method'
|
66
|
+
timeout_seconds='0'
|
67
|
+
exec='%{options/rackup} -D -p %{options/port} %{options/dir}/config.ru'>
|
68
|
+
<method_context>
|
69
|
+
<method_credential
|
70
|
+
user='%{options/user}'
|
71
|
+
group='nogroup'
|
72
|
+
privileges='basic,!proc_session,!proc_info,!file_link_any'/>
|
73
|
+
</method_context>
|
74
|
+
</exec_method>
|
75
|
+
|
76
|
+
<exec_method
|
77
|
+
name='stop'
|
78
|
+
type='method'
|
79
|
+
timeout_seconds='10'
|
80
|
+
exec='pkill -2 -f "%{options/rackup}.*-p %{options/port}"' />
|
81
|
+
|
82
|
+
</instance>
|
83
|
+
|
84
|
+
<template>
|
85
|
+
|
86
|
+
<common_name>
|
87
|
+
<loctext xml:lang='C'>
|
88
|
+
meetup-generator website
|
89
|
+
</loctext>
|
90
|
+
</common_name>
|
91
|
+
|
92
|
+
<documentation>
|
93
|
+
<doc_link name='meetup.sysdef.xyz' uri='http://meetup.sysdef.xyz' />
|
94
|
+
</documentation>
|
95
|
+
|
96
|
+
</template>
|
97
|
+
|
98
|
+
</service>
|
99
|
+
|
100
|
+
</service_bundle>
|
data/public/css/main.css
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
@font-face {
|
2
|
+
font-family: 'ZX Spectrum';
|
3
|
+
src: url('/zxspectr.woff') format('woff');
|
4
|
+
}
|
5
|
+
|
6
|
+
body {
|
7
|
+
font-family: 'ZX Spectrum';
|
8
|
+
font-size: 0.9em;
|
9
|
+
background: #000;
|
10
|
+
color: #0f0;
|
11
|
+
line-height: 120%;
|
12
|
+
}
|
13
|
+
|
14
|
+
.indent {
|
15
|
+
font-size: small;
|
16
|
+
margin-left: 12em;
|
17
|
+
}
|
18
|
+
|
19
|
+
.indent a {
|
20
|
+
color: #0f0;
|
21
|
+
}
|
22
|
+
|
23
|
+
ul {
|
24
|
+
list-style: none;
|
25
|
+
}
|
26
|
+
|
27
|
+
li {
|
28
|
+
padding-top: 1em;
|
29
|
+
text-indent: -9em;
|
30
|
+
margin-left: 9em;
|
31
|
+
}
|
32
|
+
|
33
|
+
.ttitle {
|
34
|
+
font-weight: bold;
|
35
|
+
background-color: #0ff;
|
36
|
+
color: #000;
|
37
|
+
}
|
38
|
+
|
39
|
+
h1 {
|
40
|
+
text-align: center;
|
41
|
+
padding-bottom: 0.5em;
|
42
|
+
margin-bottom: 1em;
|
43
|
+
color: #ff0;
|
44
|
+
line-height:100%;
|
45
|
+
border-bottom: 4px dotted #f0f;
|
46
|
+
}
|
47
|
+
|
48
|
+
#container {
|
49
|
+
padding-top: 3em;
|
50
|
+
width: 900px;
|
51
|
+
margin-left: auto;
|
52
|
+
margin-right: auto;
|
53
|
+
}
|
54
|
+
|
55
|
+
#footer {
|
56
|
+
margin-top: 20em;
|
57
|
+
font-size: x-small;
|
58
|
+
text-align: right;
|
59
|
+
}
|
60
|
+
|
61
|
+
a {
|
62
|
+
text-decoration: none;
|
63
|
+
color: #f0f;
|
64
|
+
}
|
data/public/favicon.ico
ADDED
Binary file
|
Binary file
|
@@ -0,0 +1,116 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
%w[minitest minitest/unit minitest/autorun rack/test pathname json
|
5
|
+
yaml cgi nokogiri].each { |f| require f }
|
6
|
+
|
7
|
+
MGROOT = Pathname.new(__FILE__).realpath.dirname.parent
|
8
|
+
|
9
|
+
OUTER_APP = Rack::Builder.parse_file((MGROOT + 'config.ru').to_s).first
|
10
|
+
|
11
|
+
# Acceptance tests for the meetup generator. Yeah, I know.
|
12
|
+
#
|
13
|
+
class TestApp < MiniTest::Test
|
14
|
+
attr_reader :things
|
15
|
+
|
16
|
+
include Rack::Test::Methods
|
17
|
+
|
18
|
+
def initialize(args)
|
19
|
+
super(args)
|
20
|
+
@things = YAML.safe_load(IO.read(MGROOT + 'lib' + 'all_the_things.yaml'),
|
21
|
+
symbolize_names: true)
|
22
|
+
end
|
23
|
+
|
24
|
+
def app
|
25
|
+
OUTER_APP
|
26
|
+
end
|
27
|
+
|
28
|
+
# Load the page a thousand times and make sure no talk titles
|
29
|
+
# occur twice
|
30
|
+
#
|
31
|
+
def test_no_repeats
|
32
|
+
1000.times do
|
33
|
+
html = Nokogiri::HTML(get('/').body)
|
34
|
+
titles = html.css('span[class="ttitle"]').map(&:text)
|
35
|
+
assert_equal(titles, titles.uniq)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Get all of the templates used to generate talk titles, and
|
40
|
+
# re-run the test until we've seen them all. Then we know no
|
41
|
+
# template causes a crash. That's good enough.
|
42
|
+
#
|
43
|
+
def test_default
|
44
|
+
templates = things[:template].map do |t|
|
45
|
+
escaped = Regexp.escape(CGI.escapeHTML(t))
|
46
|
+
matcher = escaped.gsub(/%\w+%/, '[\w\-]+').gsub(/RAND\d+/, '\d+')
|
47
|
+
.gsub(/FNOPS/, '\w+')
|
48
|
+
Regexp.new('^.*ttitle">' + matcher + '</span>.*$')
|
49
|
+
end
|
50
|
+
|
51
|
+
until templates.empty?
|
52
|
+
get '/'
|
53
|
+
assert_equal('text/html;charset=utf-8',
|
54
|
+
last_response.header['Content-Type'])
|
55
|
+
last_response.ok?
|
56
|
+
resp = last_response.body
|
57
|
+
assert_match(/The code./, resp)
|
58
|
+
assert_match(/DevOps Meetup/, resp)
|
59
|
+
templates.each { |t| templates.delete(t) if resp.match?(t) }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_api_talker
|
64
|
+
get format('/api/talker')
|
65
|
+
assert last_response.ok?
|
66
|
+
assert_instance_of(String, last_response.body)
|
67
|
+
assert_match(/^"\w+ \w+"$/, last_response.body)
|
68
|
+
assert last_response.header['Content-Type'] == 'application/json'
|
69
|
+
end
|
70
|
+
|
71
|
+
def test_api_company
|
72
|
+
get format('/api/company')
|
73
|
+
assert last_response.ok?
|
74
|
+
assert_instance_of(String, last_response.body)
|
75
|
+
assert_match(/^"\w+.io"$/, last_response.body)
|
76
|
+
assert last_response.header['Content-Type'] == 'application/json'
|
77
|
+
end
|
78
|
+
|
79
|
+
def test_api_misc
|
80
|
+
%w[title role refreshment location date].each do |word|
|
81
|
+
get format('/api/%<word>s', word: word)
|
82
|
+
assert last_response.ok?
|
83
|
+
assert_instance_of(String, last_response.body)
|
84
|
+
assert last_response.header['Content-Type'] == 'application/json'
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def test_api_talk
|
89
|
+
get format('/api/talk')
|
90
|
+
assert last_response.ok?
|
91
|
+
assert_instance_of(String, last_response.body)
|
92
|
+
as_obj = JSON.parse(last_response.body, symbolize_names: true)
|
93
|
+
assert_equal(%i[title talker role company], as_obj.keys)
|
94
|
+
end
|
95
|
+
|
96
|
+
def test_api_agenda
|
97
|
+
get format('/api/agenda')
|
98
|
+
assert last_response.ok?
|
99
|
+
assert_instance_of(String, last_response.body)
|
100
|
+
agenda = JSON.parse(last_response.body, symbolize_names: true)
|
101
|
+
assert_equal(%i[date location refreshment talks], agenda.keys.sort)
|
102
|
+
|
103
|
+
agenda[:talks].each do |t|
|
104
|
+
assert_equal(%i[title talker role company], t.keys)
|
105
|
+
end
|
106
|
+
|
107
|
+
assert_instance_of(String, agenda[:refreshment])
|
108
|
+
assert_instance_of(String, agenda[:date])
|
109
|
+
assert_instance_of(String, agenda[:location])
|
110
|
+
end
|
111
|
+
|
112
|
+
def test_api_not_found
|
113
|
+
get '/api/no_such_thing'
|
114
|
+
assert_equal(last_response.status, 404)
|
115
|
+
end
|
116
|
+
end
|