{"id":15107623,"url":"https://github.com/grype/ethel","last_synced_at":"2025-10-23T02:31:21.347Z","repository":{"id":41268417,"uuid":"140776773","full_name":"grype/Ethel","owner":"grype","description":"Lightweight framework for composing web service clients in Pharo Smalltalk","archived":false,"fork":false,"pushed_at":"2024-06-11T15:39:44.000Z","size":711,"stargazers_count":23,"open_issues_count":0,"forks_count":2,"subscribers_count":4,"default_branch":"master","last_synced_at":"2024-10-29T20:32:26.598Z","etag":null,"topics":["api","framework","graphql","pharo","pharo-smalltalk","rest","smalltalk","webservice-client"],"latest_commit_sha":null,"homepage":"","language":"Smalltalk","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/grype.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-07-13T00:17:46.000Z","updated_at":"2024-02-10T17:43:43.000Z","dependencies_parsed_at":"2024-01-12T17:35:41.381Z","dependency_job_id":"6cf8ebc7-17b0-4f4e-b001-206118f25dc4","html_url":"https://github.com/grype/Ethel","commit_stats":{"total_commits":253,"total_committers":3,"mean_commits":84.33333333333333,"dds":"0.13438735177865613","last_synced_commit":"d081e72708e2fdd445f61fbce916c0f4200f3794"},"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/grype%2FEthel","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/grype%2FEthel/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/grype%2FEthel/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/grype%2FEthel/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/grype","download_url":"https://codeload.github.com/grype/Ethel/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":237769067,"owners_count":19363250,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api","framework","graphql","pharo","pharo-smalltalk","rest","smalltalk","webservice-client"],"created_at":"2024-09-25T21:40:29.186Z","updated_at":"2025-10-23T02:31:21.038Z","avatar_url":"https://github.com/grype.png","language":"Smalltalk","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Ethel\n\n![Build status](https://travis-ci.com/grype/Ethel.svg?branch=master)\n\nLightweight framework for composing web service clients in Pharo Smalltalk. It encourages to reason about web services in terms of logical structures, promotes clean and easy to maintain architecture, and provides helpful introspective tools.\n\nEthel has a simple architecture that is able to support a wide range of APIs, including REST and GraphQL.  It can be used to write complete SDKs of varying complexity and comes with a simple DSL that allows one to quickly script interactions with REST and REST-like services.\n\n## Documentation\n* [Design](doc/design.md)\n* [Scripting](doc/scripting.md)\n* [Composing dedicated clients](doc/subclassing.md)\n* [Introspection](doc/introspection.md)\n* [Methodology](doc/methodology.md)\n\n## Getting started\n\nEthel is composed of a few packages, all of which are loaded by default. This is useful during development. However, in deployed environments you could benefit from not loading some of them, along with their dependencies.\n\n* **Ethel** - Core classes\n* **Ethel-Tests** - Test cases\n* **Ethel-Tools** - Introspective tools, code browser and inspect extensions\n* **Ethel-Examples** - Examples\n\n### Installation\n\nFor Pharo 7 and up:\n\n```smalltalk\nMetacello new\n  baseline: 'Ethel';\n  repository: 'github://grype/Ethel';\n  load.\n```\n\nIn production environments, load the ‘core’ group, which includes only the core packages and excludes all tests, examples and tools:\n\n```smalltalk\nMetacello new\n  baseline: 'Ethel';\n  repository: 'github://grype/Ethel';\n  load: #('core')\n```\n\n### Quick look\n\nLet’s use GitHub’s gist API for a quick look into some of the capabilities offered by Ethel.\n\n#### Scripting\n\n```smalltalk\nclient := WSClient jsonWithUrl: 'https://api.github.com/' httpConfiguration: [ :http |\n    http headerAt: 'Authorization' put: 'token \u003cMyAuthToken\u003e'\n].\n\n\n\"GET /gists/public - First page of public gists\"\nendpoint := client / #gists / #public.\nendpoint get.\n\n\"Enumerating gists using Collections-like API\"\nendpoint enumeration: [ :endpoint :limit :cursor |\n    | result |\n    \"Return result of #get:, and update cursor\"\n\t\tresult := endpoint get: [ :http | \n\t\t\thttp \n\t\t\t\tqueryAt: #page put: (cursor at: #page ifAbsentPut: 1);\n\t\t\t\tqueryAt: #page_size put: (cursor at: #page_size ifAbsentPut: 100)\n\t\t\t].\n\t\tcursor at: #page put: (cursor at: #page) + 1.\n\t\tcursor hasMore: result size = (cursor at: #page_size).\n\t\tresult ].\nendpoint collect: #yourself.\nendpoint select: [:each | … ] max: 10.\nendpoint detect: [:each | … ] ifFound: [ :gist | … ].\n\n\n\"POST /gists - Create a gist\"\nloadScript := 'Metacello new \n    baseline: ''Ethel''; \n    repository: ''github://grype/Ethel''; \n    load'.\n\nfiles := { ‘example.st’ -\u003e ({ #content -\u003e loadScript } asDictionary) } asDictionary.\n     \n(client / #gists)\n  post: [ :http |\n    http request contents: {\n      #description -\u003e 'Loading Ethel’.\n      #public -\u003e true.\n      #files -\u003e files } asDictionary ]\n```\n\n#### Subclassing\n\nWhen making a dedicated client, start by subclassing `WSClient` and then define endpoint classes that represent logical pieces of the API - like GitHub’s gists, for example. The framework allows both client and endpoint classes to configure HTTP request via `#configureOn:`.\n\n```smalltalk\n\"Subclass WSClient\"\nWSClient subclass: #GHRestClient\n    slots: { }\n    classVariables: {  }\n    package: ‘MyGithubApi’\n\n\"Configure http transport by adding a header\"\nWSClient\u003e\u003e#configureOn: http\n    super configureOn: http.\n    http headerAt: 'Authorization' put: 'token \u003cMyAuthToken\u003e'\n\n\"Define endpoint class for /gists\"\nObject subclass: #GHGistsEndpoint\n    uses: TWSEndpoint\n    slots: { }\n    classVariables: { }\n    package: ‘MyGithubApi’\n\n\"You must return a path relative to the client’s baseUrl via class side\"\nGHGistsEndpoint class\u003e\u003e#endpointPath\n    ^ Path / #gists\n\n\"Public gists endpoint - this one deals with paginating results\"\nObject subclass: #GHPublicGistsEndpoint\n    uses: TWSEndpoint + TWSEnumeration\n    slots: { #page. #perPage }\n    classVariables: { }\n    package: ‘MyGithubApi’\n\nGHPublicGistsEndpoint class\u003e\u003e#endpointPath\n    ^ GHGistsEndpoint endpointPath / #public\n\n\"Instance should return a new cursor object to use for pagination\"\nGHPublicGistsEndpoint\u003e\u003e#cursor\n    ^ WSPluggableCursor new. \"Or your own cursor object\"\n\n\"Fetch a single page of results using cursor\"\nGHPublicGistsEndpoint\u003e\u003e#next: limit with: cursor\n    | result |\n    page := cursor at: #page ifAbsentPut: [ 1 ].\n    \"Use limit value, if one is given, otherwise, use cursor's #perPage value\"\n    perPage := limit ifNil: [ cursor at: #perPage ifAbsentPut: 100 ].\n    result := self execute.\n    cursor at: #page put: page + 1.\n    cursor hasMore: (result size = perPage).\n    ^ result\n\n\"Configure request with pagination values\"\nGHPublicGistsEndpoint\u003e\u003econfigureOn: http\n    http \n        queryAt: #page put: page;\n        queryAt: #per_page put: perPage\n\n\"Override trait's implementation of #execute, by adding \u003cget\u003e pragma\"\nGHPublicGistsEndpoint\u003e\u003e#execute\n    \u003cget\u003e\n    ^ self execute: nil\n\n\"Connect endpoints so that we can access them from the client and other endpoints\"\nWSClient\u003e\u003e#gists\n    ^ self / GHGistsEndpoint\n\nGHGistsEndpoint\u003e\u003e#public\n    ^ self / GHPublicGistsEndpoint\n\n```\n\nThat’s essentially the bare bones of a dedicated client for GitHub’s Gists. We defined two endpoints there - one for /gists and one for /gists/public. The former isn’t really needed for the latter to exist, and was added to provide an example of endpoint composition. You can look inside `Ethel-Examples` package for a functional implementation of these examples.\n\nThere are several benefits to subclassing `WSClient` and defining concrete endpoints. For one, the interaction with the client becomes more succinct:\n\n```smalltalk\nclient := GHRestClient default.\nclient gists mine.\nclient gists public flatCollect: [:each | each at: #files] max: 10.\nclient gists \n  createWithDescription: 'GHRestClient' \n  isPublic: true \n  files: (GHRestClient methods collect: [ :each | (each selector asString , '.st') -\u003e each asString ]) asDictionary.\n```\n\nAnd if you installed **Ethel-Tools** package, you'll find additional tools to help you develop and maintain your code.\n\n#### Tools\n\n**Browser**. Endpoint classes, methods that execute http requests and methods that reference other endpoints are distinguished in the class browser:\n\n![Browser](resources/gh-browser.png)\n\n**Map**. When inspecting the client object, you get an overview of the API coverage and which parts of our implementation handle individual endpoints.\n\n![Map](resources/gh-map.png)\n\nThe map view, implemented as an extension to the inspector, distinguishes various types of implementations:\n- gray nodes represent unimplemented path segments\n- yellow nodes represent paths that are implemented by an endpoint but for which there are no executing methods (methods that actually generate an HTTP request)\n- blue nodes represent concrete implementation - which means there's at least one executing method for that path\n- filled blue nodes represent enumerating endpoints\n\n**UML**. Inspecting clients, endpoints, or their classes, you also get UML visualization.\n\n![UML](resources/gh-uml.png)\n\nThe UML view will show whatever is relevant to the inspected object. Inspecting the client object (or its class) gives the most extensive overview. Inspecting an endpoint object (or its class) limits the scope to classes that either reference or are referenced by the endpoint class. Gray lines here indicate a hierarchical relationship between classes. Blue and yellow arrows indicate references to/from other classes.\n\n**Explorer**. Exploring API using a form-like interface.\n\n![Explorer](resources/gh-explorer.png)\n![cURL](resources/gh-explorer-curl.png)\n\nThe Explorer utility uses information found in request executing methods to construct a simple form-like interface. It allows you to manually select an endpoint, fill in the arguments, fire off a request and inspect the response. The argument values are treated as code and execution of requests is done by calling actual endpoint methods. There are also buttons for inspecting the client and the selected endpoint, shortcuts to view UML and the code browser for the selected endpoint class, and a button for producing cURL command.\n\n## Etymology\n\nEthel is named after Monty Python's Ethel the Aardvark, that is all...\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgrype%2Fethel","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgrype%2Fethel","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgrype%2Fethel/lists"}