From 69cd88f4aa916bb2bd67cb69987a4a5ad361f984 Mon Sep 17 00:00:00 2001 From: Everest Date: Sat, 3 Feb 2024 23:44:58 -0300 Subject: [PATCH] Finish filters callback --- pkg/event/api.go | 55 ++++++++ pkg/event/fx.go | 2 + pkg/event/router.go | 9 ++ pkg/event/service.go | 8 +- pkg/lending/handler.go | 29 +++-- ui/package-lock.json | 91 +++++++++++++ ui/package.json | 1 + ui/src/components/EventList.tsx | 7 +- ui/src/components/Header.tsx | 93 +++++++------- .../{MapBanner.tsx => NextPageBanner.tsx} | 8 +- ui/src/molecules/ItemSelector.tsx | 94 ++++++++++++++ ui/src/organisms/FilterButton.tsx | 121 ++++++++++-------- ui/src/pages/Lending.tsx | 50 ++++++-- 13 files changed, 436 insertions(+), 132 deletions(-) create mode 100644 pkg/event/api.go create mode 100644 pkg/event/router.go rename ui/src/components/{MapBanner.tsx => NextPageBanner.tsx} (84%) create mode 100644 ui/src/molecules/ItemSelector.tsx diff --git a/pkg/event/api.go b/pkg/event/api.go new file mode 100644 index 0000000..eda73a5 --- /dev/null +++ b/pkg/event/api.go @@ -0,0 +1,55 @@ +package event + +import ( + "log/slog" + "strings" + + "github.com/labstack/echo/v4" + "github.com/samber/lo" +) + +type EventAPI struct { + service Service +} + +func NewEventAPI(service Service) *EventAPI { + return &EventAPI{ + service: service, + } +} + +type QueryParams struct { + Name string `query:"name"` + City string `query:"city"` + Tags []string `query:"tags"` + TypeOf string `query:"type_of"` + Available bool `query:"available"` + Page int `query:"page"` + Limit int `query:"limit"` +} + +func (e *EventAPI) GetEvents(c echo.Context) (err error) { + var ( + ctx = c.Request().Context() + qp QueryParams + events []Event + typeOf []EventTypeOf + typeOfSlice []string + ) + + if err = c.Bind(&qp); err != nil { + slog.ErrorContext(ctx, "Error parsing query params", "error", err.Error()) + return err + } + + if len(qp.TypeOf) > 0 { + typeOfSlice = strings.Split(qp.TypeOf, ",") + } + typeOf = lo.Map(typeOfSlice, func(i string, _ int) EventTypeOf { o, _ := ParseEventTypeOf(i); return o }) + if events, err = e.service.Get(ctx, qp.Name, qp.City, qp.Tags, typeOf, qp.Available, qp.Page, qp.Limit); err != nil { + slog.ErrorContext(ctx, "Fail to get events with this query", "error", err.Error(), "query", qp) + return err + } + + return c.JSON(200, events) +} diff --git a/pkg/event/fx.go b/pkg/event/fx.go index 6a17bb2..69a1889 100644 --- a/pkg/event/fx.go +++ b/pkg/event/fx.go @@ -5,5 +5,7 @@ import "go.uber.org/fx" func Module() fx.Option { return fx.Module("event", fx.Provide(NewEventService), + fx.Provide(NewEventAPI), + fx.Invoke(Router), ) } diff --git a/pkg/event/router.go b/pkg/event/router.go new file mode 100644 index 0000000..4add341 --- /dev/null +++ b/pkg/event/router.go @@ -0,0 +1,9 @@ +package event + +import "github.com/labstack/echo/v4" + +func Router(server *echo.Echo, handler *EventAPI) { + group := server.Group("/api/events") + + group.GET("", handler.GetEvents) +} diff --git a/pkg/event/service.go b/pkg/event/service.go index 856c976..097843d 100644 --- a/pkg/event/service.go +++ b/pkg/event/service.go @@ -3,7 +3,9 @@ package event import ( "context" "errors" + "fmt" "log/slog" + "strings" "time" "github.com/lib/pq" @@ -64,7 +66,7 @@ func (e *EventService) Get( Limit(limit) if lo.IsNotEmpty(name) { - base.Where("name like ?", name) + base.Where(fmt.Sprintf("title like '%%%s%%'", name)) } if lo.IsNotEmpty(city) { base.Where("venues.city = ?", city) @@ -73,7 +75,9 @@ func (e *EventService) Get( base.Where("tags.tag in ?", tags) } if len(typeOf) > 0 { - base.Where("? in ANY(typeOf)", typeOf) + base.Where(fmt.Sprintf("type_of <@ array[%s]", strings.Join(lo.Map(typeOf, func(i EventTypeOf, _ int) string { + return fmt.Sprintf("'%s'::eventtypeof", i.String()) + }), ","))) } if available { base.Where("begin_date <= ?", now).Where("end_date > ?", now) diff --git a/pkg/lending/handler.go b/pkg/lending/handler.go index a309435..0698601 100644 --- a/pkg/lending/handler.go +++ b/pkg/lending/handler.go @@ -3,9 +3,11 @@ package lending import ( "log/slog" "net/http" + "strings" "github.com/labstack/echo/v4" gossr "github.com/natewong1313/go-react-ssr" + "github.com/samber/lo" "github.com/marcopollivier/techagenda/lib/ssr" "github.com/marcopollivier/techagenda/pkg/event" @@ -14,25 +16,16 @@ import ( "github.com/marcopollivier/techagenda/pkg/venue" ) -type QueryParams struct { - Name string `query:"name"` - City string `query:"city"` - Tags []string `query:"tags"` - TypeOf []event.EventTypeOf `query:"type_of"` - Available bool `query:"available"` - Page int `query:"page"` - Limit int `query:"limit"` -} - func NewLendingHandler(server *echo.Echo, eventService event.Service, tagService tag.Service, venueService venue.Service, engine *ssr.Engine) { server.Static("/assets", "./ui/public/") server.Static("/favicon.ico", "./ui/public/favicon.ico") server.GET("/v2", func(c echo.Context) (err error) { var ( - ctx = c.Request().Context() - qp QueryParams - mainTag = "" + ctx = c.Request().Context() + qp event.QueryParams + mainTag = "" + typeOfSlice []string ) if err = c.Bind(&qp); err != nil { slog.ErrorContext(ctx, "Error parsing query params", "error", err.Error()) @@ -42,9 +35,17 @@ func NewLendingHandler(server *echo.Echo, eventService event.Service, tagService mainTag = qp.Tags[0] } + if len(qp.TypeOf) > 0 { + typeOfSlice = strings.Split(qp.TypeOf, ",") + } + typeOf := lo.Map(typeOfSlice, func(i string, _ int) event.EventTypeOf { + o, _ := event.ParseEventTypeOf(i) + return o + }) + tags, _ := tagService.GetAllTags(ctx) cities, _ := venueService.GetAllCities(ctx) - events, _ := eventService.Get(ctx, qp.Name, qp.City, qp.Tags, qp.TypeOf, qp.Available, qp.Page, qp.Limit) + events, _ := eventService.Get(ctx, qp.Name, qp.City, qp.Tags, typeOf, qp.Available, qp.Page, qp.Limit) page := engine.RenderRoute(gossr.RenderConfig{ File: "pages/Lending.tsx", diff --git a/ui/package-lock.json b/ui/package-lock.json index eb7bcd9..f6b0b89 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@headlessui/react": "^1.7.18", "@heroicons/react": "^2.1.1", + "axios": "^1.6.7", "moment": "^2.30.1", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -438,6 +439,21 @@ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", "dev": true }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", + "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", + "dependencies": { + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/bail": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", @@ -595,6 +611,17 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/comma-separated-tokens": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", @@ -739,6 +766,14 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -910,6 +945,25 @@ "node": ">=8" } }, + "node_modules/follow-redirects": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", @@ -938,6 +992,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2158,6 +2225,25 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minipass": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", @@ -2496,6 +2582,11 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", diff --git a/ui/package.json b/ui/package.json index 78ed02e..dbba89e 100644 --- a/ui/package.json +++ b/ui/package.json @@ -5,6 +5,7 @@ "dependencies": { "@headlessui/react": "^1.7.18", "@heroicons/react": "^2.1.1", + "axios": "^1.6.7", "moment": "^2.30.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/ui/src/components/EventList.tsx b/ui/src/components/EventList.tsx index 81c1734..7185507 100644 --- a/ui/src/components/EventList.tsx +++ b/ui/src/components/EventList.tsx @@ -1,20 +1,17 @@ -import { useState } from 'react'; import { Event } from '../props/generated'; import EventCard from '../organisms/EventCard'; +import { Filters } from '../organisms/FilterButton'; interface EventListProps { events: Event[] } export default function EventList({ events }: EventListProps) { - - const [eventsState, _] = useState(events); - return (
- {eventsState !== null ? eventsState.map((event) => ( + {events !== null ? events.map((event) => ( )) : null}
diff --git a/ui/src/components/Header.tsx b/ui/src/components/Header.tsx index 7496a61..2e0add0 100644 --- a/ui/src/components/Header.tsx +++ b/ui/src/components/Header.tsx @@ -4,7 +4,7 @@ import TechAgendaLogo from '../../public/logo.svg'; import LoginButton from '../organisms/LoginButton'; import { User } from "../props/generated"; import classNames from '../helper/classNames'; -import FilterButton from '../organisms/FilterButton'; +import { FilterButton, Filters } from '../organisms/FilterButton'; const navigation = [ { name: 'Todos os eventos', tag: "" }, @@ -20,63 +20,62 @@ interface HeaderProps { currentPage: string tags: string[] cities: string[] + onFilterChange: (state: Filters) => void } -export default function Header({ user, currentPage, tags, cities }: HeaderProps) { +export default function Header({ user, currentPage, tags, cities, onFilterChange }: HeaderProps) { return ( {({ open }) => ( - <> -
-
- {/* Logo */} -
- Tech Agenda -
- {/* User's area */} - +
+
+ {/* Logo */} +
+ Tech Agenda
-
- {/* Main menu options */} -
-
- {/* Mobile menu button*/} - - - Open main menu - {open ? ( - -
-
-
-
- {navigation.map((item) => ( - - {item.name} - - ))} -
+ {/* User's area */} + +
+
+ {/* Main menu options */} +
+
+ {/* Mobile menu button*/} + + + Open main menu + {open ? ( + +
+
+
+
+ {navigation.map((item) => ( + + {item.name} + + ))}
-
- {/* Filters menu item */} - +
+ {/* Filters menu item */} +
- +
) } diff --git a/ui/src/components/MapBanner.tsx b/ui/src/components/NextPageBanner.tsx similarity index 84% rename from ui/src/components/MapBanner.tsx rename to ui/src/components/NextPageBanner.tsx index 503b33e..b6db073 100644 --- a/ui/src/components/MapBanner.tsx +++ b/ui/src/components/NextPageBanner.tsx @@ -1,4 +1,9 @@ -export default function MapBanner() { + +interface Props { + onClick: () => void +} + +export default function NextPageBanner({ onClick }: Props) { return (
@@ -9,6 +14,7 @@ export default function MapBanner() {
Mostrar mais diff --git a/ui/src/molecules/ItemSelector.tsx b/ui/src/molecules/ItemSelector.tsx new file mode 100644 index 0000000..0a3650c --- /dev/null +++ b/ui/src/molecules/ItemSelector.tsx @@ -0,0 +1,94 @@ +import { RadioGroup } from '@headlessui/react'; + +export interface SelectorOption { + title: string + subtitle: string + value: string +} + +interface SelectorProps { + label: string + options: SelectorOption[] + selected: string + setSelected: (option: string) => void +} + +export default function ItemSelector({ label, options, selected, setSelected }: SelectorProps) { + return ( +
+
+ + {label} +
+ {options.map((option) => ( + + `${active + ? 'ring-2 ring-white/60 ring-offset-2 ring-offset-blue-500' + : '' + } + ${checked ? 'bg-blue-600/75 text-white' : 'bg-white'} + relative flex cursor-pointer rounded-lg px-5 py-4 shadow-md focus:outline-none` + } + > + {({ active, checked }) => ( + <> +
+
+
+ + {option.title} + + {option.subtitle.length > 0 ? + ( + + + {option.subtitle} + {' '} + {' '} + + ) + : null} +
+
+ {checked && ( +
+ +
+ )} +
+ + )} +
+ ))} +
+
+
+
+ ) +} + +function CheckIcon(props: any) { + return ( + + + + + ) +} diff --git a/ui/src/organisms/FilterButton.tsx b/ui/src/organisms/FilterButton.tsx index da30d04..7a2fde5 100644 --- a/ui/src/organisms/FilterButton.tsx +++ b/ui/src/organisms/FilterButton.tsx @@ -1,18 +1,20 @@ -import React, { Fragment, useState } from 'react'; -import { Dialog, Transition, Switch, Listbox, Combobox } from '@headlessui/react'; +import { Fragment, useState } from 'react'; +import { Dialog, Transition, Switch, Listbox, Combobox, RadioGroup } from '@headlessui/react'; import { AdjustmentsHorizontalIcon } from '@heroicons/react/20/solid'; import { XMarkIcon } from '@heroicons/react/24/outline'; import TagPicker from '../molecules/TagPicker'; import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/20/solid'; import classNames from '../helper/classNames'; +import ItemSelector, { SelectorOption } from '../molecules/ItemSelector' interface Props { tags: string[] cities: string[] + onChange: (state: Filters) => void } -export default function FilterButton({ tags, cities }: Props) { +export function FilterButton({ tags, cities, onChange }: Props) { const [open, setOpen] = useState(false) return ( @@ -27,7 +29,7 @@ export default function FilterButton({ tags, cities }: Props) { Filtros
- + ) } @@ -37,14 +39,50 @@ interface SideBlockProps { setOpen: (state: boolean) => void tags: string[] cities: string[] + onChange: (state: Filters) => void } -function SideBlock({ open, setOpen, tags, cities }: SideBlockProps) { +export interface Filters { + name: string + city: string + available: boolean + type_of: string + tags: string[] +} + +type ObjectKey = keyof Filters; + +function SideBlock({ open, setOpen, tags, cities, onChange }: SideBlockProps) { + + const cleanFilters: Filters = { + name: "", + city: "Todas", + available: false, + type_of: "in_person,online", + tags: [] + } - const [toggle, setToggle] = useState(false) - const [selectedTags, setSelectedTags] = useState([]) - const [eventName, setEventName] = useState('') - const [selectedCity, setselectedCity] = useState('Todas') + const typeOfOptions: SelectorOption[] = [ + { title: "Tanto faz 🤷🏼", subtitle: "", value: "in_person,online" }, + { title: "Presencial🧔🏽", subtitle: "", value: "in_person" }, + { title: "Remoto 👨🏿‍💻", subtitle: "", value: "online" } + ] + + const [filters, setFilters] = useState(cleanFilters); + + function onFilterUpdate(typeOf: ObjectKey, value: Filters[ObjectKey]): void { + let f: Filters = Object.assign({}, filters); + f[typeOf] = value as Filters[ObjectKey]; + + setFilters(f); + onChange(f); + } + + function resetFilters(): void { + let f: Filters = Object.assign({}, cleanFilters); + setFilters(f); + onChange(f); + } return ( @@ -108,9 +146,9 @@ function SideBlock({ open, setOpen, tags, cities }: SideBlockProps) { Nome do evento
- + onFilterUpdate("name", e)}> setEventName(event.target.value)} + onChange={(event) => onFilterUpdate("name", event.target.value)} className="relative w-full cursor-default rounded-md bg-white py-1.5 pl-3 pr-10 text-left text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-400 sm:text-sm sm:leading-6" /> @@ -118,58 +156,32 @@ function SideBlock({ open, setOpen, tags, cities }: SideBlockProps) {
- + onFilterUpdate("city", e)} />
-
-
- Tipo do evento -
-
-
- -
-
- -
-
-
-
- -
-
- -
-
-
-
+
+ + onFilterUpdate("type_of", e)} + />
Ainda dá pra participar? onFilterUpdate("available", e)} + className={`${filters.available ? 'bg-blue-600' : 'bg-gray-200' } relative inline-flex h-6 w-11 items-center rounded-full`} > Ainda dá pra participar? @@ -179,13 +191,14 @@ function SideBlock({ open, setOpen, tags, cities }: SideBlockProps) { - + onFilterUpdate("tags", e)} />
@@ -198,7 +211,7 @@ function SideBlock({ open, setOpen, tags, cities }: SideBlockProps) {
- + ) } diff --git a/ui/src/pages/Lending.tsx b/ui/src/pages/Lending.tsx index 0198c5f..5bfb58d 100644 --- a/ui/src/pages/Lending.tsx +++ b/ui/src/pages/Lending.tsx @@ -1,28 +1,60 @@ import { useState } from "react"; -//import { IndexRouteProps } from "../generated"; import Header from "../components/Header"; import Footer from "../components/Footer"; -import MapBanner from "../components/MapBanner"; +import NextPageBanner from "../components/NextPageBanner"; import AdBanner from "../components/AdBanner"; import EventList from "../components/EventList"; -import { Props } from "../props/generated"; +import { Props, Event as EventType } from "../props/generated"; +import { Filters } from '../organisms/FilterButton'; +import axios from 'axios'; -function Lending({ Events, User, MainTag, Tags, Cities }: Props) { +export default function Lending({ Events, User, MainTag, Tags, Cities }: Props) { const [events, setEvents] = useState(Events); - const [user, setUser] = useState(User); + const [page, setPage] = useState(0); + const [filters, setFilters] = useState({ + name: "", + city: "", + available: false, + type_of: "in_person,online", + tags: [] + }); - console.log(events) + const onFilterChange = async (f: Filters) => { + setFilters(f); + const e = await requestEvents(0, f); + setPage(0); + setEvents(e); + } + + const onRequestNewPage = async () => { + const e = await requestEvents(page + 1, filters); + setPage(page + 1); + setEvents(events.concat(e)); + } return (
-
+
- + onRequestNewPage} />
); } -export default Lending +const requestEvents = async (page: number, filters: Filters) => { + let out: EventType[] = [] + let f: Filters = Object.assign({}, filters); + f.city = f.city === 'Todas' ? '' : f.city; + + try { + const resp = await axios.get("/api/events", { params: { "page": page, ...f } }); + out = resp.data + } catch (e) { + console.log(e) + } finally { + return out + } +}