meetup-generator 0.0.20210128
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 +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
|